React + MotionFilters

Filter chips

Pill-shaped filters you can switch on and off, each one showing a small check mark as it activates.

Settings

5

Code

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Check } from "lucide-react";

const sizeMap = {
  sm: { pad: "px-3 py-1.5", text: "text-xs" },
  md: { pad: "px-4 py-2", text: "text-sm" },
  lg: { pad: "px-5 py-2.5", text: "text-base" },
} as const;

interface FilterChipsProps {
  accent?: string;
  size?: keyof typeof sizeMap;
  options?: string[];
  value?: string[];
  onChange?: (value: string[]) => void;
}

export default function FilterChips({
  accent = "#0071e3",
  size = "md",
  options = ["Design", "Code", "Audio", "Video", "3D"],
  value,
  onChange,
}: FilterChipsProps) {
  const [internal, setInternal] = useState<string[]>(options.slice(0, 1));
  const active = value ?? internal;
  const s = sizeMap[size];

  const toggle = (o: string) => {
    const next = active.includes(o) ? active.filter((x) => x !== o) : [...active, o];
    if (value === undefined) setInternal(next);
    onChange?.(next);
  };

  return (
    <div className="flex w-full max-w-md flex-wrap justify-center gap-2">
      {options.map((o) => {
        const on = active.includes(o);
        return (
          <motion.button
            key={o}
            type="button"
            aria-pressed={on}
            whileTap={{ scale: 0.94 }}
            onClick={() => toggle(o)}
            className={`flex items-center gap-1.5 rounded-full border font-medium transition-colors ${s.pad} ${s.text}`}
            style={{
              borderColor: on ? accent : "oklch(1 0 0 / 14%)",
              backgroundColor: on ? `${accent}22` : "transparent",
              color: on ? accent : undefined,
            }}
          >
            <AnimatePresence initial={false}>
              {on && (
                <motion.span
                  initial={{ width: 0, opacity: 0 }}
                  animate={{ width: "auto", opacity: 1 }}
                  exit={{ width: 0, opacity: 0 }}
                  className="overflow-hidden"
                >
                  <Check className="h-3.5 w-3.5" />
                </motion.span>
              )}
            </AnimatePresence>
            {o}
          </motion.button>
        );
      })}
    </div>
  );
}

Prompt for AI

Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.

More in Filters

Where to use it

Chips are the quickest way to let people narrow a list without opening a menu: cuisine types on a recipe site, sizes in a clothing shop, topics on a blog or skills on a job board. These filter chips allow several choices at once, and the check mark makes it obvious what is switched on.

They work best with up to eight short labels, one or two words each. If you have dozens of options, move them into a dropdown with search instead. Use the small size inside cards and the large one on mobile, where fingers need room.