UX EngineeringState PatternsFIFOFramer MotionCognitive Load

Adaptive Architectures: Designing a Bounded Selection System

Minimizing cognitive load through friction-free rolling selection windows.

Users are often overwhelmed by large sets of selectable options (Y items) when they are constrained to a limited subset (X selections).


Real-World Use Cases

  • E-commerce Product Comparison: A customer wants to compare 3 laptops out of a catalog of 200. Instead of forcing them to deselect "Laptops A" before choosing "Laptop D", an adaptive system rolls the comparison window.
  • Portfolio Management: An investor selecting 5 top-performing stocks for a quick overview dashboard.
  • Ad Campaigns: A marketer pinning 3 "High Priority" campaigns from an active list.
  • Social Apps: Selecting exactly 3 "Best Friends" to share a story with, where the system keeps your most frequent/recent choices.

Typical systems create:

  • Decision Fatigue: The pressure of choosing the 'perfect' X items.
  • Hard-Stop Errors: Disruptive modals or alerts saying "You can't select more".
  • Interaction Friction: Requiring users to manually deselect an item before adding a new one.

Step-by-Step Implementation Guide

  1. Define the Bound: Determine the maximum number of items allowed (X).
  2. Initialize Ordered State: Use a Set or an OrderedMap to track selections.
  3. Implement the Transition:
    • If the item exists: Remove it.
    • If the item is new:
      • If under the limit: Add it.
      • If at the limit: Evict the oldest entry (FIFO) and then add the new one.
  4. Provide Visual Cues: Use animations (like fading or scale-down) to indicate which item is being evicted.
  5. Persist & Rehydrate: Sync the state with localStorage to ensure the user's choices aren't lost on refresh.

Solution Analysis

Approach A: Restrictive (Naive)

In a typical first-pass implementation, developers often use a standard array-toggle pattern:

const [selections, setSelections] = useState<string[]>([]);
const toggle = (id) => {
  if (selections.includes(id)) {
    setSelections((prev) => prev.filter((i) => i !== id));
  } else if (selections.length < LIMIT) {
    setSelections((prev) => [...prev, id]);
  } else {
    alert('Selection limit reached!'); // The dreaded hard-stop
  }
};

The Issues:

  1. User Frustration: Forcing a manual "deselect" step breaks the user's flow.
  2. Scalability: array.includes and array.filter become $O(N)$ operations which lag on large datasets (1,000+ items).
  3. Rigid Logic: It doesn't allow for flexible strategies like LRU (Least Recently Used) or FIFO (First In, First Out) without significant boilerplate.

Approach B: Adaptive (Set-Based)

By switching to a Set, we gain O(1) lookup performance and leverage the fact that JavaScript Sets maintain insertion order, providing a natural FIFO queue.

const toggle = (id) => {
  setSelections((prev) => {
    const next = new Set(prev);
    if (next.has(id)) {
      next.delete(id);
    } else {
      next.add(id);
      if (next.size > LIMIT) {
        // Naturally removes the oldest insertion
        const oldest = next.values().next().value;
        next.delete(oldest);
      }
    }
    return next;
  });
};

Advanced Pattern: Priority Locking

What happens if a user wants to roll their selection window but keep specific items permanently?

In a standard FIFO model, the oldest item is always the first to go. A more sophisticated approach introduces a Locking (Pinning) layer.

The Algorithm: Lock-Aware FIFO

  1. Check the Bound: If the limit (X) is reached and a new item is added.
  2. Search the State: Instead of just grabbing the first element of the Set (the oldest), iterate through the set to find the oldest element that is not locked.
  3. Eviction Logic:
    • If an unlocked candidate is found: Evict it and insert the new item.
    • If all items are locked: Revert to a manual block or "Hard-Stop" (as there is no legitimate way to auto-manage the state without breaking user intent).
const toggle = (id) => {
  setSelections(prev => {
    const next = new Set(prev);
    if (next.has(id)) {
       next.delete(id);
    } else {
       if (next.size >= LIMIT) {
         // Search for first unlocked candidate
         const targetId = Array.from(next).find(item => !locked.has(item));
         
         if (targetId) {
            next.delete(targetId);
         } else {
            return prev; // All locked, hard-stop
         }
       }
       next.add(id);
    }
    return next;
  });
};

Interactive Prototype: Try the Solution

Experience the friction-free rolling selection window and the Advanced Locking Mode in action, featuring 1000+ item scalability and priority pins.