Tag Group

An interactive multi-select filter system featuring fluid layout transitions and reactive states. Utilizing Framer Motion for background interpolation and icon scaling, it provides a high-contrast, tactile way to categorize content within an architectural B/W interface.

framer-motionlucide-react

Filter by Topic

Installation

Install the required dependencies:

npm install framer-motion lucide-react
components/ui/tag-group.tsx
"use client";
  import React, { useState } from "react";
  import { motion } from "framer-motion";
  import { Hash, X } from "lucide-react";
  
  const SUGGESTED_TAGS = [
    "React",
    "Next.js",
    "Tailwind",
    "Framer",
    "TypeScript",
    "Backend",
    "UI/UX",
  ];
  
  export function TagGroup({ tags = SUGGESTED_TAGS }) {
    const [selectedTags, setSelectedTags] = useState<string[]>(["React"]);
  
    const toggleTag = (tag: string) => {
      setSelectedTags((prev) =>
        prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
      );
    };
  
    return (
      <div className="p-6 rounded-4xl border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-900/30">
        <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 mb-4">
          Filter by Topic
        </h4>
        <div className="flex flex-wrap gap-2">
          {tags.map((tag) => {
            const isSelected = selectedTags.includes(tag);
            return (
              <motion.button
                key={tag}
                layout
                onClick={() => toggleTag(tag)}
                animate={{
                  backgroundColor: isSelected ? "#18181b" : "transparent",
                  color: isSelected ? "#ffffff" : "#71717a",
                }}
                className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl border text-xs font-bold transition-colors ${
                  isSelected
                    ? "border-zinc-900 dark:bg-white dark:text-black dark:border-white"
                    : "border-zinc-200 dark:border-zinc-800 hover:border-zinc-400"
                }`}
              >
                <Hash
                  size={12}
                  className={isSelected ? "text-zinc-400" : "text-zinc-500"}
                />
                {tag}
                {isSelected && (
                  <motion.span initial={{ scale: 0 }} animate={{ scale: 1 }}>
                    <X size={12} />
                  </motion.span>
                )}
              </motion.button>
            );
          })}
        </div>
      </div>
    );
  }