Velocity UI
Loading…
Menu

Liquid Filters

A filter navigation component with a fluid background indicator that morphs seamlessly between selected items.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add liquid-filters

Source Code

liquid-filters.tsx
'use client'

import React, { useState } from 'react'
import { motion, LayoutGroup } from 'framer-motion'

interface LiquidFiltersProps {
  items?: string[]
  defaultSelected?: string
  onSelect?: (item: string) => void
}

const DEFAULT_FILTERS = [
  'All',
  'Design',
  'Development',
  'Marketing',
  'Strategy',
  'Analytics'
]

export default function LiquidFilters({ 
  items = DEFAULT_FILTERS, 
  defaultSelected = 'All',
  onSelect 
}: LiquidFiltersProps) {
  const [selected, setSelected] = useState(defaultSelected)

  const handleSelect = (filter: string) => {
    setSelected(filter)
    onSelect?.(filter)
  }

  return (
    <div className="w-full h-[400px] bg-neutral-100 dark:bg-neutral-950 flex flex-col items-center justify-center gap-8 font-sans">
      
      {/* Filter Bar */}
      <div className="flex flex-wrap items-center justify-center gap-2 p-2 bg-white dark:bg-neutral-900 rounded-full shadow-lg border border-neutral-200 dark:border-white/5">
        <LayoutGroup>
          {items.map((filter) => {
            const isSelected = selected === filter
            return (
              <motion.button
                key={filter}
                layout
                onClick={() => handleSelect(filter)}
                className={`relative px-4 py-2 rounded-full text-sm font-medium transition-colors z-10 ${
                    isSelected ? 'text-white dark:text-black' : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white'
                }`}
              >
                {isSelected && (
                  <motion.div
                    layoutId="activeFilter"
                    className="absolute inset-0 bg-black dark:bg-white rounded-full -z-10"
                    transition={{ type: "spring", stiffness: 300, damping: 30 }}
                  />
                )}
                {filter}
              </motion.button>
            )
          })}
        </LayoutGroup>
      </div>

      {/* Content Placeholder */}
      <motion.div 
        key={selected}
        initial={{ opacity: 0, y: 20 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: -20 }}
        className="text-center"
      >
        <div className="text-6xl font-bold text-neutral-200 dark:text-neutral-800 mb-4">
            {selected.substring(0, 2)}
        </div>
        <p className="text-neutral-500">Showing results for <span className="font-semibold text-black dark:text-white">{selected}</span></p>
      </motion.div>

    </div>
  )
}

Dependencies

  • framer-motion: latest
  • lucide-react: latest

Props

Component property reference.

NameTypeDefaultDescription
itemsstring[]['All', 'Design', 'Development', ...]Array of filter labels.
defaultSelectedstring'All'Initially selected filter.
onSelect(item: string) => voidundefinedCallback when a filter is selected.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.