Skip to content
Back to Design Hub

Favorites & Bookmarks Pattern

Letting users save items for later with optimistic UI, persistence, and empty states.

UX PatternsBeginner

Why

Users find an item they want to revisit but don't want to search again. Favorites give them a persistent 'saved' list — a high-engagement feature that's cheap to build and works offline.

Nielsen Norman Group research shows users spend an average of 10-15 seconds on a page before leaving. Clear visual hierarchy and progressive disclosure improve task completion rates by 40-60%.

When

Any app with browsable content users might revisit: products, articles, properties, jobs, search results. Skip for one-shot content (receipts, confirmations).

How

1. Toggle UI — star/heart icon, optimistic flip, filled vs outline state 2. Store — user-scoped list of item IDs; persist to backend (if logged in) + localStorage (offline) 3. Sync — on login, merge local favorites with server; on toggle, write both 4. List view — /favorites page showing saved items, sorted by date saved 5. Empty state — "No favorites yet — tap the ☆ on any item to save it" with a CTA to browse 6. Limit — cap at 1000 favorites; beyond that, suggest folders/collections instead 6. Export — let users export their favorites (CSV/JSON) so they don't feel locked in 7. Remove — un-favorite from the list view (swipe or trash icon), not just the item page

Example

// useFavorites — optimistic toggle, dual-store function useFavorites(userId?: string) { const [ids, setIds] = useState<Set<string>>(() => loadLocal()); const toggle = (id: string) => { const next = new Set(ids); next.has(id) ? next.delete(id) : next.add(id); // optimistic setIds(next); saveLocal(next); if (userId) api.put(`/users/${userId}/favorites`, { ids: [...next] }).catch(() => setIds(ids)); // rollback }; return { ids, toggle, isFav: (id) => ids.has(id) }; } // Star button — optimistic, accessible <button aria-pressed={isFav(id)} aria-label={isFav(id) ? "Remove from favorites" : "Add to favorites"} onClick={() => toggle(id)} > <StarIcon filled={isFav(id)} /> </button> // Empty state function FavoritesEmpty() { return ( <div> <h2>No favorites yet</h2> <p>Tap the ☆ on any item to save it for later.</p> <Link to="/browse">Browse items</Link> </div> ); }

Common Problems

  • ⚠No offline storage — favorites vanish on logout or network loss, users feel betrayed
  • ⚠Empty state is just 'No favorites' — no guidance, users don't know how to add items
  • ⚠No merge on login — local favorites overwritten by server, or vice versa, items lost

Best Practices

  • ✓Dual-store (localStorage + server) — works offline, syncs on login, survives device switch
  • ✓Optimistic toggle with rollback — instant feedback, reverts if the server fails
  • ✓Design a helpful empty state — explain how to add favorites, link to browse

FAQ