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.