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