Most online stores lose sales in the same place: the category page. A shopper lands on 480 products, cannot narrow them down fast enough, and leaves. Good ecommerce product filter design is the difference between a catalog that feels like a warehouse and one that feels like a personal shopper.
This guide is a practical breakdown of the three layout patterns you will actually choose between (sidebar, horizontal top bar, mobile bottom sheet), plus the details that make or break them: facet ordering, applied-filter chips, result counts, CSS layout snippets and accessibility rules for checkboxes, sliders and clear-all controls.
The three filter patterns at a glance
Before picking a pattern, be honest about your catalog. The right choice depends on how many facets you have, how visual your grid is, and how much of your traffic is on a phone.
| Pattern | Best for | Strengths | Weaknesses |
|---|---|---|---|
| Sidebar (left rail) | Deep catalogs, 6 to 20+ facets (fashion, hardware, auto parts, B2B) | All facets visible at once, scannable, easy multi-select, no hidden state | Steals 240 to 300px of grid width, can push products below the fold |
| Horizontal top bar | Shallow catalogs, 3 to 7 facets, image-led brands | Full-width product grid, works as a sticky bar, feels light | Facet values hidden in dropdowns, poor for 10+ facets, more clicks |
| Mobile bottom sheet | Every store, on screens under 768px | Thumb-reachable, full-height space, keeps context behind the overlay | Needs careful focus trapping, scroll locking and an explicit apply flow |
| Hybrid (sidebar + sticky toolbar) | Mid to large catalogs with heavy sorting use | Facets in the rail, sort and applied chips in the bar, best of both | More components to build and test |
Short answer: if you have more than seven facets, use a sidebar on desktop and a bottom sheet on mobile. If you have fewer, a top bar keeps the grid wide and the page feeling fast.

Pattern 1: The sidebar filter
The left rail is still the default for a reason. Shoppers have been trained by two decades of retail sites to look left. It shows facet labels and values at the same time, which means less clicking and fewer forgotten options.
When the sidebar wins
- You have 8 or more facets and shoppers regularly combine three or four of them.
- Your facets have long value lists (brand, size, material) that need scroll or search inside the group.
- Your average order requires precision: parts fitment, technical specs, sizing.
Sidebar layout rules that matter
- Keep the rail between 240px and 300px. Narrower breaks long brand names, wider eats the grid.
- Make it sticky with its own scroll so it never runs out before the product list does.
- Show the first 5 to 8 values per facet, then a “Show 12 more” toggle. Never hide values behind a scrollbar alone.
- Keep the top three facets expanded by default; collapse the rest with clear accordion affordances.
- Filter on click, not on a submit button, on desktop. Update the grid without a full page repaint.
CSS: a resilient sidebar layout
.plp {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 2rem;
align-items: start;
}
.plp__filters {
position: sticky;
top: 88px; /* height of your sticky header */
max-height: calc(100vh - 104px);
overflow-y: auto;
overscroll-behavior: contain;
padding-right: .5rem;
}
.plp__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.5rem;
}
@media (max-width: 1024px) {
.plp { grid-template-columns: 1fr; }
.plp__filters { position: static; max-height: none; }
}
The minmax(0, 1fr) on the second column is not optional. Without it, a long product title or a wide image can blow out the grid and push the layout sideways. Anyone digging further should read 10 Ways to Optimize eCommerce Product Filters.
Pattern 2: The horizontal top bar
A top bar turns each facet into a button that opens a dropdown panel. It gives you a full-width grid, which is why lifestyle and fashion brands love it. The trade-off is discoverability: values are hidden until clicked.
Making a top bar work
- Cap it at five to seven facet buttons. Beyond that, add an “All filters” button that opens a full panel or drawer.
- Show the active count on the button, for example
Size (3). Without it, applied filters become invisible. - Open panels anchored under the button, not centered on screen. Position matters for spatial memory.
- Use a multi-column layout inside the panel for long value lists so users do not scroll a 40 item column.
- Make the bar sticky, but keep it slim (56 to 64px) so it does not fight with the header.
CSS: sticky top bar with anchored panels
.filterbar {
position: sticky;
top: 72px;
z-index: 20;
display: flex;
flex-wrap: wrap;
gap: .5rem;
padding: .75rem 0;
background: #fff;
border-block: 1px solid #e5e5e5;
}
.filterbar__item { position: relative; }
.filterbar__panel {
position: absolute;
inset-inline-start: 0;
top: calc(100% + .5rem);
min-width: 320px;
max-height: 60vh;
overflow-y: auto;
padding: 1rem;
background: #fff;
border: 1px solid #e5e5e5;
border-radius: 10px;
box-shadow: 0 12px 32px rgba(0,0,0,.12);
columns: 2;
column-gap: 1.5rem;
}
.filterbar__panel[hidden] { display: none; }
Use the native hidden attribute or the Popover API rather than visibility tricks. Hidden panels must be removed from the accessibility tree, not just visually dimmed. Someone has put together a good summary of it.

Pattern 3: The mobile bottom sheet
On mobile, filters belong at the bottom of the screen where thumbs live. A bottom sheet that slides up over the results is now the expected pattern, and it beats the old full-screen page that erased all context.
Mobile filter rules
- Put a persistent Filter and Sort bar at the bottom of the viewport, or sticky under the header, with the active filter count visible.
- Inside the sheet, use two levels: facet list first, then values, or accordions if you have fewer than eight facets.
- Always end with a sticky action bar: “Clear all” on the left, “Show 128 results” on the right.
- The primary button must contain the live result count. This is the single highest-impact detail on mobile filters.
- Lock background scroll while the sheet is open, and restore scroll position when it closes.
- Support swipe-down to dismiss, but never make it the only way out. Keep a visible close button.
CSS: bottom sheet with a native dialog
.sheet {
border: 0;
padding: 0;
width: 100%;
max-width: 100%;
max-height: 88vh;
margin: auto auto 0;
border-radius: 16px 16px 0 0;
animation: sheet-up .22s ease-out;
}
.sheet::backdrop { background: rgba(0,0,0,.45); }
.sheet__body { overflow-y: auto; padding: 1rem; }
.sheet__actions {
position: sticky;
bottom: 0;
display: flex;
gap: .75rem;
padding: .75rem 1rem calc(.75rem + env(safe-area-inset-bottom));
background: #fff;
border-top: 1px solid #e5e5e5;
}
@keyframes sheet-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.sheet { animation: none; }
}
Open it with dialog.showModal(). You get focus trapping, Escape to close and backdrop handling for free, which removes a whole category of accessibility bugs.
Facet ordering: the part everyone gets wrong
Layout is the easy half. The hard half is deciding which facets appear, in what order, on each category. A generic global filter list is the most common failure in ecommerce filter UI design.
Order by decision sequence, not by database
Shoppers filter in a predictable order. Mirror it:
- Category or type refinement (if the user has not already drilled down)
- Hard constraints that eliminate products entirely: size, fitment, compatibility, dietary needs
- Price (always in the top three, always visible without expanding)
- Brand, if brand loyalty is real in your vertical
- Visual or taste attributes: color, style, material, pattern
- Nice-to-have flags: rating, in stock, on sale, free shipping, new arrivals
Category-specific facets are non negotiable
A “Running Shoes” page needs drop, cushioning and terrain. A “Sofas” page needs seat count, upholstery and dimensions. Showing “Screen Size” on a sofa page tells shoppers your site does not understand its own products.
- Hide facets with a single value. If every product is black, do not show a color filter.
- Hide facets with zero coverage. A facet that only 12% of the products carry produces frustrating empty results.
- Order values sensibly: sizes numerically, ratings descending, everything else by result count descending.
- Add a search box inside any facet with 15+ values (typically brand).
Color and size deserve custom controls
Color swatches beat text labels, but only with a label on hover, focus and in the accessible name. Size should be a grid of tappable chips, not a checkbox list. Price works best as a set of preset ranges plus an optional two-input min and max, and the manual inputs must be usable without touching the slider.

Applied filter chips: the memory of the page
Once a shopper applies three or four filters, they need a single place to see and undo them. Chips (also called tokens or pills) sit above the product grid and answer “what am I looking at right now?”.
Chip rules
- Place them directly above the grid, below the H1, on every breakpoint.
- Label the facet, not just the value:
Color: Navyis clearer thanNavy. - Each chip needs its own remove control with an accessible name like Remove filter Color Navy. An X icon alone is not a label.
- Add a Clear all control at the end of the row, visually distinct from the chips so it is never mistaken for one.
- Keep chips on one row with horizontal scroll on mobile, or wrap to a max of two rows with a “+3 more” expander.
- Removing a chip must also uncheck the matching control in the panel. State must be one source of truth.
<ul class="chips" aria-label="Applied filters">
<li class="chip">
Color: Navy
<button type="button" class="chip__x" aria-label="Remove filter Color Navy">
<span aria-hidden="true">×</span>
</button>
</li>
</ul>
<button type="button" class="chips__clear">Clear all filters</button>
Result counts: show the consequence before the click
Counts are the cheapest trust signal in the whole interface. They tell the shopper what will happen before they commit, which prevents the dreaded dead end of zero results.
| Where | What to show | Why |
|---|---|---|
| Next to each facet value | Navy (42) | Sets expectations and prevents empty selections |
| Above the grid | 128 products | Confirms the filter actually did something |
| Mobile sheet button | Show 128 results | Removes the need to close the sheet to check |
| Zero-result state | Which filter caused it + one-tap removal | Turns a dead end into a recovery path |
Disable or dim values that would return zero results, but keep them visible with a count of 0 rather than deleting them. Options that vanish and reappear make the interface feel unstable.

Accessibility: checkboxes, sliders and clear-all
Filters are one of the most keyboard-hostile components on the web when built badly. Here is what an audit will look for.
Checkbox and radio groups
- Use real
<input type="checkbox">elements. Do not rebuild them from divs withrole="checkbox". - Wrap each facet in a
<fieldset>with a<legend>naming the facet, so screen readers announce “Color, Navy, checkbox, not checked”. - Keep hit targets at least 44 by 44 CSS pixels, and make the whole label row clickable.
- Never remove the focus ring. Style it with
:focus-visibleand a 3:1 contrast outline. - If results update instantly, announce the change with a polite live region.
<fieldset class="facet">
<legend class="facet__title">Color</legend>
<label class="facet__row">
<input type="checkbox" name="color" value="navy">
<span>Navy</span>
<span class="facet__count">(42)</span>
</label>
</fieldset>
<p class="sr-only" role="status" aria-live="polite">128 products match your filters</p>
Accordions for collapsible facets
- The toggle must be a
<button>witharia-expandedandaria-controls. - The heading structure should be logical:
<h3>containing the button, inside a filter region labelledaria-label="Product filters". - Collapsed content must be truly hidden so it is skipped by tab order.
Price sliders
Sliders are the most commonly broken filter control. Follow these rules:
- Provide text inputs for min and max alongside the slider. Some users will never drag anything.
- Each handle needs
role="slider"(or a native range input), plusaria-valuemin,aria-valuemax,aria-valuenowandaria-valuetextwith the formatted currency. - Support Arrow keys for single steps, Page Up and Page Down for larger jumps, Home and End for the extremes.
- Give each handle a distinct accessible name: Minimum price and Maximum price.
- Debounce the request by roughly 300ms so dragging does not fire a hundred queries.
- Handles need a minimum 24px touch target even if the visual dot is smaller.
Clear-all and remove controls
- Use a real button with a descriptive name: Clear all filters, not just Clear.
- After clearing, move focus predictably: back to the filter region heading or the results heading, never to
<body>. - Announce the outcome in the live region: All filters cleared, 480 products.
- Only show Clear all when at least one filter is active. A permanently visible dead control is confusing.
- Do not require a confirmation dialog. Make the action instantly reversible via browser back instead.
URLs, indexing and speed
Filter design is also an SEO decision. Every combination you expose can create a crawlable URL, and thousands of thin variants will drain your crawl budget. baymard.com goes into the numbers.
- Push filter state into the URL with
history.pushStateso results are shareable and the back button works. This is a usability win first, an SEO win second. - Choose a small set of indexable combinations, typically single-facet pages with real search demand (“navy running shoes”), and give them unique titles and intro copy.
- Canonicalize or noindex the rest, especially multi-facet, sort and pagination-only variants.
- Keep parameter order stable so the same selection never generates two different URLs.
- Render the first page of results server side. Filters that depend entirely on client-side JavaScript risk being invisible to crawlers and slow on mid-range phones.
- Target an interaction response under 200ms. Optimistic UI plus skeleton cards feels dramatically faster than a blocking spinner.

A pre-launch checklist
- Filters are reachable and operable with keyboard only, start to finish.
- Every facet value shows a result count, and zero-count values are visibly disabled.
- Applied chips appear above the grid, each individually removable.
- Clear all exists, is only shown when relevant, and moves focus correctly.
- Mobile sheet has a sticky apply button containing the live result count.
- Filter state survives the back button and a page refresh.
- Zero results shows which filter is responsible plus a one-tap fix.
- Facet lists are category specific, not a global dump.
- Slider has text inputs and full keyboard support.
- Layout does not shift when the results update (watch your CLS).
So, sidebar or top bar?
If we had to give one recommendation for a typical mid-size store: a sticky left sidebar on desktop from 1024px up, a sticky toolbar holding sort plus applied chips, and a native dialog bottom sheet below 1024px. It handles catalog growth, it is the pattern shoppers already know, and it is the easiest of the three to make fully accessible.
Choose the top bar when your catalog is genuinely shallow and the visual grid is your main selling tool. Just do not force it once your facet count creeps past seven, because that is when the hidden-value problem starts costing conversions.
Need a category page that actually converts? The team at FatCow Web Design builds and audits ecommerce interfaces with performance, accessibility and search visibility handled from the first wireframe. Get in touch and tell us about your catalog.
FAQ: ecommerce product filter design
Should filters apply instantly or after clicking an Apply button?
On desktop, apply instantly. The results are visible next to the controls, so feedback is immediate. On mobile, where the sheet covers the results, use an explicit Apply button that carries the live result count so users still see the consequence before committing.
How many filters should an ecommerce category page have?
Show between four and eight facets by default and keep the rest behind a “More filters” expander. Coverage matters more than quantity: a facet is only worth showing if the majority of products on that page actually have a value for it.
What is the difference between filtering and sorting?
Filtering removes products from the list based on attributes. Sorting reorders the same list. Keep them visually separate: sort belongs in the toolbar above the grid, filters belong in the sidebar or sheet. Mixing them in one dropdown confuses shoppers.
Should filter values that return zero results be hidden?
Keep them visible but disabled, with a count of 0. Hiding them makes the list shift under the user’s finger and makes the interface feel unpredictable. The exception is a value with zero results across the entire category, which should not be rendered at all.
Do product filters hurt SEO?
Only when every combination is left crawlable. Pick a handful of single-facet pages that match real search demand and make them proper landing pages with unique content, then canonicalize or noindex the remaining combinations and block low-value parameters from crawling.
What is the best filter pattern for mobile?
A bottom sheet opened from a persistent Filter and Sort bar. It keeps controls in the thumb zone, preserves context behind the overlay, and gives you room for a sticky action bar with Clear all and a result-count Apply button.
Are price sliders a good idea?
They are fine as a secondary control, but never as the only one. Pair the slider with preset price ranges and editable min and max inputs. Sliders alone are hard to use precisely on touch screens and are frequently inaccessible to keyboard and screen reader users.
