Filter Components

Beautiful, responsive filter components built with TailwindCSS. Alpine.js previews with matching Vue and React code. UI-only filter patterns that are easy to copy, paste, and customize for any project.

1) Product Filter Bar

Horizontal filter bar with category, price range, and sort options. UI-only for demonstration.

Products

results
Active Filters:
<div x-data="productFilter()" class="max-w-4xl mx-auto">
  <!-- Filter Header -->
  <div class="flex flex-wrap items-center justify-between gap-4 mb-6">
    <h3 class="text-lg font-semibold">Products</h3>
    <div class="flex items-center gap-2 text-sm text-gray-600">
      <span x-text="filteredCount"></span> results
    </div>
  </div>

  <!-- Filter Controls -->
  <div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
    <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
      <!-- Category Filter -->
      <div>
        <label class="block text-sm font-medium mb-2">Category</label>
        <select x-model="selectedCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-violet focus:border-violet">
          <option value="">All Categories</option>
          <option value="electronics">Electronics</option>
          <option value="clothing">Clothing</option>
          <option value="books">Books</option>
        </select>
      </div>
      <!-- Add other selects... -->
    </div>

    <!-- Active Filters -->
    <div class="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-gray-200">
      <span class="text-sm font-medium text-gray-700">Active Filters:</span>
      <template x-if="selectedCategory">
        <span class="inline-flex items-center gap-1 px-3 py-1 bg-violet text-white text-xs rounded-full">
          <span x-text="selectedCategory"></span>
          <button @click="selectedCategory = ''" type="button">×</button>
        </span>
      </template>
      <template x-if="hasActiveFilters">
        <button @click="clearAllFilters()" class="text-xs text-gray-600 hover:text-gray-800 underline">Clear all</button>
      </template>
    </div>
  </div>

  <!-- Results -->
  <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
    <template x-for="product in filteredProducts" :key="product.id">
      <div class="border border-gray-200 rounded-lg p-4">
        <h4 class="font-semibold" x-text="product.name"></h4>
        <p class="text-sm text-gray-600" x-text="product.brand"></p>
        <p class="font-bold text-violet" x-text="'$' + product.price"></p>
      </div>
    </template>
  </div>
</div>

<script>
function productFilter() {
  return {
    selectedCategory: '',
    selectedPriceRange: '',
    selectedBrand: '',
    sortBy: 'name',
    products: [...], // product data
    get filteredProducts() { /* filtering logic */ },
    get filteredCount() { return this.filteredProducts.length; },
    get hasActiveFilters() { return this.selectedCategory || this.selectedPriceRange || this.selectedBrand; },
    clearAllFilters() { this.selectedCategory = ''; /* reset others */ }
  }
}
</script>
<template>
  <div class="max-w-4xl mx-auto">
    <!-- Filter Header -->
    <div class="flex flex-wrap items-center justify-between gap-4 mb-6">
      <h3 class="text-lg font-semibold">Products</h3>
      <div class="flex items-center gap-2 text-sm text-gray-600">
        <span>{{ filteredCount }}</span> results
      </div>
    </div>

    <!-- Filter Controls -->
    <div class="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
      <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
        <div>
          <label class="block text-sm font-medium mb-2">Category</label>
          <select v-model="selectedCategory" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-violet focus:border-violet">
            <option value="">All Categories</option>
            <option value="electronics">Electronics</option>
            <option value="clothing">Clothing</option>
            <option value="books">Books</option>
          </select>
        </div>
        <!-- Other selects... -->
      </div>

      <!-- Active Filters -->
      <div class="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-gray-200">
        <span class="text-sm font-medium text-gray-700">Active Filters:</span>
        <span v-if="selectedCategory" class="inline-flex items-center gap-1 px-3 py-1 bg-violet text-white text-xs rounded-full">
          <span>{{ selectedCategory }}</span>
          <button @click="selectedCategory = ''" type="button">×</button>
        </span>
        <button v-if="hasActiveFilters" @click="clearAllFilters" class="text-xs text-gray-600 hover:text-gray-800 underline">Clear all</button>
      </div>
    </div>

    <!-- Results -->
    <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
      <div v-for="product in filteredProducts" :key="product.id" class="border border-gray-200 rounded-lg p-4">
        <h4 class="font-semibold">{{ product.name }}</h4>
        <p class="text-sm text-gray-600">{{ product.brand }}</p>
        <p class="font-bold text-violet">${{ product.price }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const selectedCategory = ref('')
const selectedPriceRange = ref('')
const selectedBrand = ref('')
const sortBy = ref('name')

const products = [
  { id: 1, name: 'iPhone 15', brand: 'Apple', category: 'electronics', price: 799 },
  { id: 2, name: 'Running Shoes', brand: 'Nike', category: 'clothing', price: 120 },
  // ... more products
]

const filteredProducts = computed(() => {
  let filtered = products
  
  if (selectedCategory.value) {
    filtered = filtered.filter(p => p.category === selectedCategory.value)
  }
  
  if (selectedBrand.value) {
    filtered = filtered.filter(p => p.brand.toLowerCase() === selectedBrand.value.toLowerCase())
  }
  
  return filtered.sort((a, b) => {
    switch(sortBy.value) {
      case 'price-low': return a.price - b.price
      case 'price-high': return b.price - a.price
      default: return a.name.localeCompare(b.name)
    }
  })
})

const filteredCount = computed(() => filteredProducts.value.length)
const hasActiveFilters = computed(() => selectedCategory.value || selectedPriceRange.value || selectedBrand.value)

const clearAllFilters = () => {
  selectedCategory.value = ''
  selectedPriceRange.value = ''
  selectedBrand.value = ''
}
</script>
import React, { useState, useMemo } from 'react'

export default function ProductFilter() {
  const [selectedCategory, setSelectedCategory] = useState('')
  const [selectedPriceRange, setSelectedPriceRange] = useState('')
  const [selectedBrand, setSelectedBrand] = useState('')
  const [sortBy, setSortBy] = useState('name')

  const products = [
    { id: 1, name: 'iPhone 15', brand: 'Apple', category: 'electronics', price: 799 },
    { id: 2, name: 'Running Shoes', brand: 'Nike', category: 'clothing', price: 120 },
    // ... more products
  ]

  const filteredProducts = useMemo(() => {
    let filtered = products
    
    if (selectedCategory) {
      filtered = filtered.filter(p => p.category === selectedCategory)
    }
    
    if (selectedBrand) {
      filtered = filtered.filter(p => p.brand.toLowerCase() === selectedBrand.toLowerCase())
    }
    
    return filtered.sort((a, b) => {
      switch(sortBy) {
        case 'price-low': return a.price - b.price
        case 'price-high': return b.price - a.price
        default: return a.name.localeCompare(b.name)
      }
    })
  }, [selectedCategory, selectedBrand, sortBy])

  const hasActiveFilters = selectedCategory || selectedPriceRange || selectedBrand

  const clearAllFilters = () => {
    setSelectedCategory('')
    setSelectedPriceRange('')
    setSelectedBrand('')
  }

  return (
    <div className="max-w-4xl mx-auto">
      {/* Filter Header */}
      <div className="flex flex-wrap items-center justify-between gap-4 mb-6">
        <h3 className="text-lg font-semibold">Products</h3>
        <div className="flex items-center gap-2 text-sm text-gray-600">
          <span>{filteredProducts.length}</span> results
        </div>
      </div>

      {/* Filter Controls */}
      <div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-6">
        <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
          <div>
            <label className="block text-sm font-medium mb-2">Category</label>
            <select value={selectedCategory} onChange={(e) => setSelectedCategory(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-violet focus:border-violet">
              <option value="">All Categories</option>
              <option value="electronics">Electronics</option>
              <option value="clothing">Clothing</option>
              <option value="books">Books</option>
            </select>
          </div>
          {/* Other selects... */}
        </div>

        {/* Active Filters */}
        <div className="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-gray-200">
          <span className="text-sm font-medium text-gray-700">Active Filters:</span>
          {selectedCategory && (
            <span className="inline-flex items-center gap-1 px-3 py-1 bg-violet text-white text-xs rounded-full">
              <span>{selectedCategory}</span>
              <button onClick={() => setSelectedCategory('')} type="button">×</button>
            </span>
          )}
          {hasActiveFilters && (
            <button onClick={clearAllFilters} className="text-xs text-gray-600 hover:text-gray-800 underline">Clear all</button>
          )}
        </div>
      </div>

      {/* Results */}
      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
        {filteredProducts.map(product => (
          <div key={product.id} className="border border-gray-200 rounded-lg p-4">
            <h4 className="font-semibold">{product.name}</h4>
            <p className="text-sm text-gray-600">{product.brand}</p>
            <p className="font-bold text-violet">${product.price}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

2) Sidebar Filter Panel

Vertical sidebar layout with checkboxes, range sliders, and radio buttons for advanced filtering.

Filters

Categories

Price Range

$ $

Rating

Products

results found
<div class="flex gap-6" x-data="sidebarFilter()">
  <!-- Sidebar Filter -->
  <div class="w-64 flex-shrink-0 bg-gray-50 border border-gray-200 rounded-lg p-4">
    <h3 class="text-lg font-semibold mb-4">Filters</h3>
    
    <!-- Categories -->
    <div class="mb-6">
      <h4 class="font-medium mb-3">Categories</h4>
      <div class="space-y-2">
        <template x-for="category in categories" :key="category">
          <label class="flex items-center">
            <input type="checkbox" x-model="selectedCategories" :value="category" class="rounded border-gray-300 text-violet focus:ring-violet">
            <span class="ml-2 text-sm" x-text="category"></span>
          </label>
        </template>
      </div>
    </div>

    <!-- Price Range -->
    <div class="mb-6">
      <h4 class="font-medium mb-3">Price Range</h4>
      <div class="space-y-3">
        <div class="flex items-center justify-between text-sm">
          <span>$<span x-text="priceRange[0]"></span></span>
          <span>$<span x-text="priceRange[1]"></span></span>
        </div>
        <input type="range" x-model="priceRange[0]" min="0" max="1000" step="10" class="w-full">
        <input type="range" x-model="priceRange[1]" min="0" max="1000" step="10" class="w-full">
      </div>
    </div>

    <button @click="clearFilters()" class="w-full px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-md hover:bg-gray-300 transition-colors">
      Clear All Filters
    </button>
  </div>

  <!-- Results -->
  <div class="flex-1">
    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      <template x-for="product in filteredProducts" :key="product.id">
        <div class="border border-gray-200 rounded-lg p-4">
          <h4 class="font-semibold" x-text="product.name"></h4>
          <p class="font-bold text-violet" x-text="'$' + product.price"></p>
        </div>
      </template>
    </div>
  </div>
</div>
<template>
  <div class="flex gap-6">
    <div class="w-64 flex-shrink-0 bg-gray-50 border border-gray-200 rounded-lg p-4">
      <h3 class="text-lg font-semibold mb-4">Filters</h3>
      
      <div class="mb-6">
        <h4 class="font-medium mb-3">Categories</h4>
        <div class="space-y-2">
          <label v-for="category in categories" :key="category" class="flex items-center">
            <input type="checkbox" v-model="selectedCategories" :value="category" class="rounded border-gray-300 text-violet focus:ring-violet">
            <span class="ml-2 text-sm">{{ category }}</span>
          </label>
        </div>
      </div>

      <button @click="clearFilters" class="w-full px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-md">
        Clear All Filters
      </button>
    </div>

    <div class="flex-1">
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        <div v-for="product in filteredProducts" :key="product.id" class="border border-gray-200 rounded-lg p-4">
          <h4 class="font-semibold">{{ product.name }}</h4>
          <p class="font-bold text-violet">${{ product.price }}</p>
        </div>
      </div>
    </div>
  </div>
</template>
export default function SidebarFilter() {
  const [selectedCategories, setSelectedCategories] = useState([])
  const [priceRange, setPriceRange] = useState([0, 1000])
  const [selectedRating, setSelectedRating] = useState(null)
  
  const categories = ['Electronics', 'Clothing', 'Books', 'Home & Garden']
  
  const filteredProducts = useMemo(() => {
    let filtered = products
    
    if (selectedCategories.length > 0) {
      filtered = filtered.filter(p => selectedCategories.includes(p.category))
    }
    
    filtered = filtered.filter(p => p.price >= priceRange[0] && p.price <= priceRange[1])
    
    if (selectedRating) {
      filtered = filtered.filter(p => p.rating >= selectedRating)
    }
    
    return filtered
  }, [selectedCategories, priceRange, selectedRating])

  return (
    <div className="flex gap-6">
      <div className="w-64 flex-shrink-0 bg-gray-50 border border-gray-200 rounded-lg p-4">
        <h3 className="text-lg font-semibold mb-4">Filters</h3>
        
        <div className="mb-6">
          <h4 className="font-medium mb-3">Categories</h4>
          <div className="space-y-2">
            {categories.map(category => (
              <label key={category} className="flex items-center">
                <input type="checkbox" checked={selectedCategories.includes(category)} onChange={(e) => {
                  if (e.target.checked) {
                    setSelectedCategories([...selectedCategories, category])
                  } else {
                    setSelectedCategories(selectedCategories.filter(c => c !== category))
                  }
                }} className="rounded border-gray-300 text-violet focus:ring-violet" />
                <span className="ml-2 text-sm">{category}</span>
              </label>
            ))}
          </div>
        </div>

        <button onClick={() => {
          setSelectedCategories([])
          setPriceRange([0, 1000])
          setSelectedRating(null)
        }} className="w-full px-4 py-2 bg-gray-200 text-gray-800 font-medium rounded-md">
          Clear All Filters
        </button>
      </div>

      <div className="flex-1">
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
          {filteredProducts.map(product => (
            <div key={product.id} className="border border-gray-200 rounded-lg p-4">
              <h4 className="font-semibold">{product.name}</h4>
              <p className="font-bold text-violet">${product.price}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

3) Search & Tag Filter

Compact filter with search input and clickable tag buttons for quick filtering.

Quick Filters:
Active filters:

Results

items found

No items match your search criteria

<div x-data="searchTagFilter()">
  <!-- Search Input -->
  <div class="relative mb-4">
    <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
      <svg class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
      </svg>
    </div>
    <input x-model="searchTerm" type="text" placeholder="Search products..." class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet focus:border-violet">
  </div>

  <!-- Filter Tags -->
  <div class="flex flex-wrap gap-2 mb-4">
    <span class="text-sm font-medium text-gray-700">Quick Filters:</span>
    <template x-for="tag in availableTags" :key="tag">
      <button @click="toggleTag(tag)" :class="activeTags.includes(tag) ? 'bg-violet text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'" class="px-3 py-1 rounded-full text-sm font-medium transition-colors">
        <span x-text="tag"></span>
      </button>
    </template>
  </div>

  <!-- Results -->
  <div class="space-y-3">
    <template x-for="item in filteredItems" :key="item.id">
      <div class="flex items-center gap-4 p-3 border border-gray-200 rounded-lg">
        <div class="w-12 h-12 bg-gray-100 rounded-lg flex-shrink-0"></div>
        <div class="flex-1">
          <h4 class="font-medium text-sm" x-text="item.name"></h4>
          <p class="text-xs text-gray-500" x-text="item.category"></p>
        </div>
        <p class="font-bold text-violet text-sm" x-text="'$' + item.price"></p>
      </div>
    </template>
  </div>
</div>
<template>
  <div>
    <div class="relative mb-4">
      <input v-model="searchTerm" type="text" placeholder="Search products..." class="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet focus:border-violet">
    </div>

    <div class="flex flex-wrap gap-2 mb-4">
      <span class="text-sm font-medium text-gray-700">Quick Filters:</span>
      <button v-for="tag in availableTags" :key="tag" @click="toggleTag(tag)" :class="activeTags.includes(tag) ? 'bg-violet text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'" class="px-3 py-1 rounded-full text-sm font-medium transition-colors">
        {{ tag }}
      </button>
    </div>

    <div class="space-y-3">
      <div v-for="item in filteredItems" :key="item.id" class="flex items-center gap-4 p-3 border border-gray-200 rounded-lg">
        <div class="w-12 h-12 bg-gray-100 rounded-lg flex-shrink-0"></div>
        <div class="flex-1">
          <h4 class="font-medium text-sm">{{ item.name }}</h4>
          <p class="text-xs text-gray-500">{{ item.category }}</p>
        </div>
        <p class="font-bold text-violet text-sm">${{ item.price }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const searchTerm = ref('')
const activeTags = ref([])
const availableTags = ['Popular', 'Sale', 'New', 'Premium', 'Bestseller']

const items = [
  { id: 1, name: 'MacBook Pro', category: 'Electronics', price: 1299, tags: ['Popular', 'Premium'] },
  // ... more items
]

const filteredItems = computed(() => {
  let filtered = items
  
  if (searchTerm.value) {
    const term = searchTerm.value.toLowerCase()
    filtered = filtered.filter(item => 
      item.name.toLowerCase().includes(term) ||
      item.category.toLowerCase().includes(term)
    )
  }
  
  if (activeTags.value.length > 0) {
    filtered = filtered.filter(item =>
      activeTags.value.some(tag => item.tags.includes(tag))
    )
  }
  
  return filtered
})

const toggleTag = (tag) => {
  const index = activeTags.value.indexOf(tag)
  if (index > -1) {
    activeTags.value.splice(index, 1)
  } else {
    activeTags.value.push(tag)
  }
}
</script>
export default function SearchTagFilter() {
  const [searchTerm, setSearchTerm] = useState('')
  const [activeTags, setActiveTags] = useState([])
  
  const availableTags = ['Popular', 'Sale', 'New', 'Premium', 'Bestseller']
  const items = [
    { id: 1, name: 'MacBook Pro', category: 'Electronics', price: 1299, tags: ['Popular', 'Premium'] },
    // ... more items
  ]
  
  const filteredItems = useMemo(() => {
    let filtered = items
    
    if (searchTerm) {
      const term = searchTerm.toLowerCase()
      filtered = filtered.filter(item => 
        item.name.toLowerCase().includes(term) ||
        item.category.toLowerCase().includes(term)
      )
    }
    
    if (activeTags.length > 0) {
      filtered = filtered.filter(item =>
        activeTags.some(tag => item.tags.includes(tag))
      )
    }
    
    return filtered
  }, [searchTerm, activeTags])
  
  const toggleTag = (tag) => {
    setActiveTags(prev => 
      prev.includes(tag) 
        ? prev.filter(t => t !== tag)
        : [...prev, tag]
    )
  }

  return (
    <div>
      <div className="relative mb-4">
        <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} type="text" placeholder="Search products..." className="block w-full pl-10 pr-3 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet focus:border-violet" />
      </div>

      <div className="flex flex-wrap gap-2 mb-4">
        <span className="text-sm font-medium text-gray-700">Quick Filters:</span>
        {availableTags.map(tag => (
          <button key={tag} onClick={() => toggleTag(tag)} className={`px-3 py-1 rounded-full text-sm font-medium transition-colors ${activeTags.includes(tag) ? 'bg-violet text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'}`}>
            {tag}
          </button>
        ))}
      </div>

      <div className="space-y-3">
        {filteredItems.map(item => (
          <div key={item.id} className="flex items-center gap-4 p-3 border border-gray-200 rounded-lg">
            <div className="w-12 h-12 bg-gray-100 rounded-lg flex-shrink-0" />
            <div className="flex-1">
              <h4 className="font-medium text-sm">{item.name}</h4>
              <p className="text-xs text-gray-500">{item.category}</p>
            </div>
            <p className="font-bold text-violet text-sm">${item.price}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Notes

  • This is a UI-only demonstration showing filter interface patterns.
  • HTML preview uses Alpine.js for interactivity. Make sure Alpine is loaded in your project.
  • Vue/React versions provide the same visual experience with framework-specific state management.
  • Adapt the filtering logic to work with your actual data source (API, database, etc.).
  • Active filter pills allow users to easily remove individual filters.

We use cookies to improve your experience and analytics. You can accept all cookies or reject non-essential ones.

Learn More