Dynamic Filters

February 2025

I really like the idea of transforming simple things into something perfect for the user's eye through animation. This is a great way to create selected multi filters with micro interaction.

Animation

This component was made entirely with Motion. So, I had to create three variants: left, center, and alone.

const variants: AnimationProps["variants"] = {
  alone: {
    borderRadius: "9999px",
    marginLeft: "12px",
    paddingLeft: "8px",
  },
  left: {
    borderTopLeftRadius: "0px",
    borderTopRightRadius: "9999px",
    borderBottomLeftRadius: "0px",
    borderBottomRightRadius: "9999px",
    marginLeft: "-14px",
    paddingLeft: "16px",
  },
  center: {
    borderTopLeftRadius: "0px",
    borderTopRightRadius: "0px",
    borderBottomLeftRadius: "0px",
    borderBottomRightRadius: "0px",
    borderRightColor: "transparent",
    borderLeftColor: "transparent",
    marginLeft: "-14px",
    paddingLeft: "16px",
  },
};

The alone variant is used when no filter on the side is activated. In this case, the style applied includes a borderRadius of 100% and a marginLeft to ensure that the component isn't "stuck" to other elements.

The left variant is used when a filter is activated on the left side. Here, the component's borderLeftRadius is nullified, and a negative marginLeft is applied to create the “sticky” effect.

Finally, the center variant is positioned between two elements, resulting in a borderRadius of 0.

How to make this work?

This function is responsible for determining the appropriate style, as explained above.

const getStyle = useCallback(
  (filter: string): "alone" | "left" | "center" => {
    const filterIndex = Object.keys(filters).indexOf(filter);

    if (!filters[filter]) return "alone";

    const prevFilter = filters[Object.keys(filters)[filterIndex - 1]];
    const nextFilter = filters[Object.keys(filters)[filterIndex + 1]];

    if (prevFilter && nextFilter) return "center";

    if (prevFilter) return "left";

    return "alone";
  },
  [filters],
);

Implementation

The FilterItem component represents each filter button and uses the isStyle prop to determine its animation state.

type FilterItemProps = {
  children: React.ReactNode;
  active?: boolean;
  isStyle: "left" | "center" | "alone";
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type"> &
  MotionProps;
	
function FilterItem({
  children,
  active,
  isStyle,
  className,
  ...props
}: FilterItemProps) {
  return (
    <motion.button
      className={cn(
        "ml-3 inline-flex h-8 items-center justify-center",
        "rounded-full border border-neutral-300 px-2 text-sm",
        "text-neutral-600 dark:border-neutral-800 dark:text-neutral-200",
        active
          ? "bg-neutral-100 dark:bg-[#171717]"
          : "bg-background !text-neutral-400 dark:!text-foreground",
        active &&
          isStyle === "alone" &&
          "bg-neutral-100 bg-opacity-[.97] text-neutral-600 dark:bg-[#171717] dark:text-neutral-200",
        isStyle === "alone" && "!border-neutral-300 dark:!border-neutral-800",
        className,
      )}
      variants={variants}
      animate={isStyle}
      transition={{ type: "tween", duration: 0.15 }}
      {...props}
    >
      {children}
    </motion.button>
  );
}

Managing Filters

Finally, the DynamicFilters function manages the active filter state and applies styles dynamically.

export function DynamicFilters() {
  const FILTERS = {
    Playlists: false,
    Albums: false,
    Podcasts: false,
    Artists: false,
    Downloaded: false,
  };

  const [filters, setFilters] = useState<
		Record<string, boolean>>(FILTERS);

  function handleFilter(filter: string) {
    setFilters((prev) => ({ ...prev, [filter]: !prev[filter] }));
  }

  <div className="flex">
    {Object.entries(filters).map(([filter, active], index) => (
      <FilterItem
        key={filter}
        active={active}
        onClick={() => handleFilter(filter)}
        className="last:hidden sm:last:block"
        isStyle={getStyle(filter)}
        style={{ zIndex: Object.keys(filters).length - index }}
      >
        {filter}
      </FilterItem>
    ))}
  </div>;
}

Inspiration

I discovered Paco's Spotify Filters and was immediately captivated. A few times in side projects, I had to work with multi-select filters, something I've always thought I could improve. This inspired me to explore how I could enhance them, leading to the creation of this component.

One last thing

I hope this guide has been helpful. If you use this user interface in your app, feel free to tag me on Twitter or LinkedIn.