Dynamic Filtering
Users don’t want to scroll through every service. They want to filter by category — show me just lawn care, just consultations. This combines everything you’ve learned: state for the selected filter, conditions for empty states, and loops for rendering results.
What You’ll Learn
- Track selected category with state
- Filter arrays based on selection
- Handle filtered empty states
- Build a complete filter UI
The Building Blocks
We need:
- Services data — The full list of services
- Selected category state — Which filter is active
- Filtered services — Computed from the full list
- Category buttons — To change the filter
- Service display — The filtered results
Set Up the Data
Start with services that have category information:
<script>
const services = [
{ id: '1', slug: 'lawn-mowing', name: 'Lawn Mowing', category: 'lawn-care', price: 50 },
{ id: '2', slug: 'hedge-trimming', name: 'Hedge Trimming', category: 'lawn-care', price: 75 },
{ id: '3', slug: 'garden-consultation', name: 'Garden Consultation', category: 'consultation', price: 100 },
{ id: '4', slug: 'tree-pruning', name: 'Tree Pruning', category: 'tree-care', price: 150 },
{ id: '5', slug: 'patio-cleaning', name: 'Patio Cleaning', category: 'cleaning', price: 80 }
];
const categories = [
{ id: 'all', name: 'All Services' },
{ id: 'lawn-care', name: 'Lawn Care' },
{ id: 'tree-care', name: 'Tree Care' },
{ id: 'consultation', name: 'Consultation' },
{ id: 'cleaning', name: 'Cleaning' }
];
let selectedCategory = $state('all');
</script> Compute Filtered Services
Use $derived to automatically filter based on selection:
<script>
// ... services and categories data ...
let selectedCategory = $state('all');
let filteredServices = $derived(
selectedCategory === 'all'
? services
: services.filter(s => s.category === selectedCategory)
);
</script> When selectedCategory changes, filteredServices automatically recalculates. We’ll cover $derived in depth in Module 10 — for now, know that it creates computed values that update reactively.
Build the Filter UI
Create buttons for each category:
<div class="category-filters">
{#each categories as category (category.id)}
<button
class:active={selectedCategory === category.id}
onclick={() => selectedCategory = category.id}
>
{category.name}
</button>
{/each}
</div> The class:active directive adds the active class when the condition is true — highlighting the selected filter.
Display Filtered Results
Combine the filter with the service list:
<div class="services-list">
{#each filteredServices as service (service.id)}
<article class="service-card">
<h3>{service.name}</h3>
<p class="category">{service.category}</p>
<p class="price">${service.price}</p>
<a href="/services/{service.slug}">View Details</a>
</article>
{:else}
<div class="empty-state">
<p>No services found in this category.</p>
<button onclick={() => selectedCategory = 'all'}>
View All Services
</button>
</div>
{/each}
</div> The {:else} handles empty filtered results.
The Complete Component
Here’s everything together:
<!-- filename: src/routes/services/+page.svelte -->
<script>
const services = [
{ id: '1', slug: 'lawn-mowing', name: 'Lawn Mowing', category: 'lawn-care', price: 50 },
{ id: '2', slug: 'hedge-trimming', name: 'Hedge Trimming', category: 'lawn-care', price: 75 },
{ id: '3', slug: 'garden-consultation', name: 'Garden Consultation', category: 'consultation', price: 100 },
{ id: '4', slug: 'tree-pruning', name: 'Tree Pruning', category: 'tree-care', price: 150 },
{ id: '5', slug: 'patio-cleaning', name: 'Patio Cleaning', category: 'cleaning', price: 80 }
];
const categories = [
{ id: 'all', name: 'All Services' },
{ id: 'lawn-care', name: 'Lawn Care' },
{ id: 'tree-care', name: 'Tree Care' },
{ id: 'consultation', name: 'Consultation' },
{ id: 'cleaning', name: 'Cleaning' }
];
let selectedCategory = $state('all');
let filteredServices = $derived(
selectedCategory === 'all'
? services
: services.filter(s => s.category === selectedCategory)
);
</script>
<h1>Our Services</h1>
<div class="category-filters">
{#each categories as category (category.id)}
<button
class:active={selectedCategory === category.id}
onclick={() => selectedCategory = category.id}
>
{category.name}
</button>
{/each}
</div>
<p class="results-count">
Showing {filteredServices.length} of {services.length} services
</p>
<div class="services-list">
{#each filteredServices as service (service.id)}
<article class="service-card">
<h3>
<a href="/services/{service.slug}">{service.name}</a>
</h3>
<span class="category-badge">{service.category}</span>
<p class="price">${service.price}</p>
</article>
{:else}
<div class="empty-state">
<p>No services found in this category.</p>
<button onclick={() => selectedCategory = 'all'}>
View All Services
</button>
</div>
{/each}
</div>
<style>
.category-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.category-filters button {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
background: white;
border-radius: 20px;
cursor: pointer;
}
.category-filters button:hover {
border-color: #007bff;
}
.category-filters button.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.results-count {
color: #666;
margin-bottom: 1rem;
}
.services-list {
display: grid;
gap: 1rem;
}
.service-card {
padding: 1rem;
border: 1px solid #eee;
border-radius: 8px;
}
.service-card h3 {
margin: 0 0 0.5rem 0;
}
.service-card a {
color: #333;
text-decoration: none;
}
.service-card a:hover {
color: #007bff;
}
.category-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #f0f0f0;
border-radius: 4px;
font-size: 0.875rem;
color: #666;
}
.price {
font-weight: bold;
color: #007bff;
}
.empty-state {
text-align: center;
padding: 2rem;
background: #f8f9fa;
border-radius: 8px;
}
</style> Test the Interaction
- Load the services page — all 5 services show
- Click “Lawn Care” — 2 services show
- Click “Tree Care” — 1 service shows
- Click “Consultation” — 1 service shows
- Click “All Services” — back to 5 services
The count updates, the list filters, and the active button highlights. All reactive, all automatic.
Common Mistakes
Mutating the Original Array
<!-- ❌ Don't modify the source array -->
let filteredServices = $derived(
services.filter(s => s.category === selectedCategory)
);
// If you did: services = services.filter(...) — you'd lose data!
<!-- ✅ Filter returns a new array, original is preserved -->
let filteredServices = $derived(
services.filter(s => s.category === selectedCategory)
); Forgetting the Empty State
Always handle the case where filtering produces zero results. Users need feedback.
Not Using Keys
<!-- ❌ Without keys, filtering can cause glitches -->
{#each filteredServices as service}
<!-- ✅ With keys, each item is tracked correctly -->
{#each filteredServices as service (service.id)} Summary
Filtering combines state, derived values, conditions, and loops. The selected category is state, filtered services are derived, the empty case is a condition, and rendering is a loop. These primitives compose into powerful interactive features.
Key takeaways:
- Use state for the selected filter
- Use
$derivedto compute filtered results - Always handle empty filtered states
- Use keyed each blocks for dynamic lists
Module Complete! 🎉
You’ve finished Module 5: Control Flow. BookIt now has:
- Understanding of Svelte’s control flow blocks
- Conditional rendering with
{#if},{:else}, and{:else if} - Empty state handling for better UX
- List rendering with
{#each} - Keyed lists for correct updates
- Interactive category filtering
What you’ve learned:
- Svelte’s template syntax for control flow
{#if}/{:else}/{:else if}for conditional content- Designing thoughtful empty states
{#each}for rendering arrays- Keys for proper list tracking
- Combining control flow patterns
Next Steps
Forms need more than text inputs. Continue with Module 6: Data Binding to explore checkboxes, radio buttons, selects, and more.