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:
None
<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:
Search: ""
Results
items foundNo 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.