Filtering navigated away from the list, so people lost their place and stopped filtering
A compact shopping app built around one interaction decision: brand, category and filter selection happen in modals over the list, so the browse position survives and people actually narrow their search.
Who, what, and how long
E-commerce & Retail
7 weeks
Fixed price
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
What we were brought in to do
The brief called for a small, fast shopping app, not a wrapper around the retailer's site. We built it: browse, product detail, bag, favorites and account, with every narrowing action in a modal over the list.
A retailer wanting a small, fast shopping app, not a wrapper around their site. The previous app's filters pushed a new screen and returned to the top of a freshly loaded list, so anyone forty products into a scroll lost all of it. They learned that after doing it twice and stopped filtering, which showed up in the analytics as disinterest in a well-designed feature.
Mobile Engineering
Where the old way broke
In their previous app, tapping Filter pushed a new screen. Applying it popped back to the top of a freshly loaded list. Someone forty products into a scroll lost all of it, learned that after doing it twice, and stopped using filters. So they saw fewer relevant products and bought less.
Nobody reported it. Users don't complain about a feature that costs them their place; they simply stop opening it, and the resulting metric (low filter engagement) reads as a design or discovery problem when it's really a penalty. The team had spent two releases redesigning the filter sheet on that reading.
Filters, brand and category are modals presented over the list. The list stays mounted underneath, so applying a filter narrows what's already there, and dismissing without applying costs nothing.
Filters are modals
Narrowing happens over the list, so the browse position and loaded pages survive.
Filters, brand and category are presented as modals over the list, so the list stays mounted with its loaded pages and scroll position intact. Applying a filter narrows what's already on screen. The previous navigation-based version threw away twenty loaded products and a minute of browsing every time someone opened the filter sheet.
- Presented over the list; the list stays mounted
- Loaded pages and scroll position survive a filter change
- Narrows what's on screen, in place
The filters sheet raised over the list as a modal: two brands chosen for 45 products, and the browse list still there underneath at item 41.
The brand modal being closed without a choice: eight brands with their counts, and nothing reloads, so the list behind it is still at item 41 of 128.
Dismissal is free
Opening filters just to look costs nothing, which is why people open them.
Dismissing without applying costs nothing (no reload, no lost position, no re-fetch), and that property decides whether people open filters at all. When looking is expensive, nobody looks; the filter set had been well designed and almost unused for exactly that reason.
- Dismiss without applying is free: no reload or refetch
- Opening filters to look costs nothing
- Turned a well-designed, unused feature into a used one
Products are addressable
The detail route takes an id, so a shared link opens the product itself.
The product detail route takes an id, so every product has a real address that opens directly from a shared link, a notification or a deep link out of search. Sharing used to land people on the home screen, so a customer sending a friend a product was really sending them a shop and a hunt.
- Detail route keyed by product id: every product addressable
- Shared links and notifications open the product itself
- No more landing on the home screen from a share
Product detail reached by id from a shared link, so the link opens the wrap coat itself, with its colors, UK sizes and add to bag.
Favourites is a tab
Saved items on their own tab, six favorites with one reduced and one already in the bag, because opening favorites is a different intent from narrowing a browse.
A different intent gets a different place of its own.
Favorites is a tab, not a filter, because saving something and narrowing a list are different intents, and folding one into the other makes both harder to reach. A tab also gives favorites its own state and its own empty state, which is where the prompt to start saving actually belongs.
- A different intent gets its own place, never a filter value
- Its own state and its own empty state
- The prompt to start saving lands where it belongs
Small on purpose
Five surfaces. A shopping app that takes a while to open is a shopping app people don't open.
Five surfaces, deliberately. Every extra screen is startup cost, bundle size and one more decision for the customer, and a shopping app that takes a moment to open is one people stop opening. Cuts were judged on that basis, whatever a feature's individual merits.
- Five surfaces total, sized against startup cost
- Cuts made on startup cost, not individual merit
- An app that opens slowly is an app that goes unopened
The account tab: four orders with their status and the settings behind them. With browse, product detail, the bag and saved, it's the fifth and last surface in the app.
What we built together
Presented every narrowing action as a modal over the list, never a pushed screen
The presentation decision came first because it determines how browse state is held, and everything else in the app follows from that.
Kept the list mounted underneath, so applying a filter never reloads from the top
Applying narrows what's already on screen in place, the difference between refining a browse and starting a new one.
Made dismissal free, so opening filters just to look isn't a decision
Dismissing without applying costs nothing (no reload, no lost position), which makes opening filters just to look a free action.
Kept favorites on its own tab, separate from filters, because it's a different intent
Favorites is a tab with its own state and its own empty state, kept out of the filter set, because saving and narrowing are different intents.
Built the product route to accept an id directly, so a shared link opens the product
The detail route takes a product id, so a shared link or a notification opens the product itself.
Operational results after launch
0
List reloads on filter
Never
Scroll position lost
None
Cost of opening filters
5
Surfaces in the app
List reloads on filter is zero: the list stays mounted. Scroll position lost is never. Cost of opening filters is none, the property that decides whether people open them. Surfaces in the app is a count, and a deliberate one.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
Phase by phase
Phase 1: Browse
The List That Stays
Built the product list as the app's home surface, holding its scroll position and loaded pages across every interaction short of a navigation.
- Product List
- Paging
- Scroll Retention
Phase 2: The Modals
Narrowing In Place
Built brand, category and filter selection as modals over the list. The list is never remounted, so applying a filter narrows it in place.
- Filter Modal
- Brand Modal
- Category Modal
Phase 3: Product And Bag
Detail Without Losing Browse
Built product detail as a route by id, so it can be linked to directly, and the bag as a tab: the two places where a push is the right answer.
- Product Detail
- Bag
- Deep Links
Phase 4: Favourites
A Tab, Not A Filter
Kept favorites as its own tab. It looks like a filter and isn't: someone opening favorites has a different intent from someone narrowing a browse.
- Favourites
- Sync
- Empty State
The bag: two lines, a £20 promotion applied, free delivery, and a £234.00 total shown with the £39.00 of VAT it already includes.
About our collaboration
They brought session recordings instead of a brief. Watching three people filter once, lose their place and never filter again settled the design question in about ten minutes and made the rest of the engagement straightforward.
Seven weeks, fixed, with the presentation decision made first because it determines everything about how browse state is held. Scope was actively defended: every extra surface proposed was weighed against startup cost, on the grounds that a shopping app that takes a moment to open is one people stop opening.
What we'd carry into the next one
A pushed screen unmounts what's behind it. For anything that narrows a list, that's the wrong presentation.
A pushed screen unmounts what's behind it. That's right for a destination and wrong for anything whose job is to narrow what you're already looking at.
Users don't tell you they stopped using a feature. They just stop, and the analytics look like disinterest.
People don't report abandoning a feature, so the metric shows disinterest, and two releases had been spent redesigning a sheet that was never the problem.
Make exploratory actions free to abandon, or people won't start them.
Exploration only happens when abandoning is free. The moment looking has a cost, users stop looking and the feature's usage collapses.
Favorites is an intent, not a filter, and folding it into the filter set hides it from the people who want it.
Folding favorites into the filter set hides an intent inside a mechanism, and the people who wanted it were the least likely to look for it there.
Forty products in, then a filter
The list never reloads, because it never leaves the screen.
One browse session in four steps: scrolled to item 41, filters opened as a modal, a brand filter applied, and the sheet opened again and dismissed. Keep an eye on the reload counter and the scroll position, and on what the previous app did at the same moment. Switch steps, or use the arrow keys once a step is focused.
One browse session · coats & jackets, 128 products
Products and list positions after filtering are illustrative- 41Wool-blend wrap coatAldery£189.00
- 42Quilted liner jacketNorrow£98.00
- 43Car coatPellow£145.00
- 44Cotton trenchSelwyn£165.00
- 45Padded giletQuillon£72.00
- 46Double-faced coatAldery£229.00
The list is the home surface. It holds its scroll position and its loaded pages across every interaction short of a navigation.
The same starting point in both apps: a shopper forty products into a scroll.
- Step 1 · reloads 0
- Step 2 · reloads 0
- Step 3 · reloads 0
- Step 4 · reloads 0
A presentation decision, made first, that decides how browse state is held
A pushed screen unmounts what's behind it, which is right for a destination and wrong for anything that narrows what you're already looking at. So narrowing is a modal, and the one real destination, the product, is a route by id.
- 01 · TriggerA tap on Filter, Brand or Category, or a shared linkEvery narrowing action is presented as a modal. A shared link, a notification or a deep link out of search carries a product id.
- 02 · RoutingExpo Router: modals and a product route by idFilter, brand and category are modals, never pushed screens. Product detail takes an id, so every product has a real address.
- 03 · NarrowingNarrow what is already on screenApplying a filter narrows the mounted list instead of replacing it: 0 list reloads. Dismissing without applying re-fetches nothing.
- 04 · StateThe list that stays mountedLoaded pages and scroll position survive every interaction short of a navigation. Favorites keeps its own state on its own tab.
- 05 · ClientReact Native app, 5 surfacesBrowse, Product detail, Bag, Saved, Account. Built with Expo and TypeScript over a REST API, sized against startup cost.
What a shopping app must never break
Browse position, shared links & the bag total
Nobody loses their place
Filters, brand and category are modals over a list that stays mounted, so applying one causes 0 list reloads, the scroll position is never lost, and closing without applying costs nothing. Opening filters to look is no longer a gamble.
A shared link opens the product
The product detail route takes an id, so a link sent to a friend, a notification or a deep link out of search opens that product, with no home screen and no hunt.
The bag shows its working
Each line, the promotion as its own line, delivery, and the total with the tax already inside it: £234.00 including £39.00 VAT, shown as already included rather than left for the shopper to work out.
Shoppers losing their place every time they filter? Scope your build in three minutes.
Scope your buildNearby engagements
Data & AnalyticsA checkout that stopped losing sales to a 6-second load
Profiling found the real bottleneck behind a slow checkout (an N+1 query and an oversized bundle) and tuned both, with before-and-after metrics locked in as a baseline against future regressions.
E-commerce & Retail · 5 weeks
Web PlatformsA reader people finish, and a library that remembers where they stopped
A reading platform for a comics catalog: a browse surface people can actually navigate, a reader that gets out of the way, and a history that puts everyone back on the page they left.
E-commerce & Retail · 18 weeks
Web PlatformsOne number, a whole handset, and a database that keeps up with 125 brands
A metered IMEI lookup service that turns fifteen digits into a device, its specifications and its status. It's sold three ways to three audiences and backed by a device database that maintains itself.
Consumer Electronics · 22 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














