What behavioral science reveals about engagement loops, digital habits, and the responsibility of developers.
The software that doesn't want you to leave
You open an app to check one thing: whether a package shipped, what time the game starts, a reply from a friend. You find it in seconds. Then a post below it catches your eye, and another below that, and when you next look at the clock twenty minutes have gone somewhere you never decided to send them.
Nothing malfunctioned. The feed did precisely what it was built, tuned and measured to do, and those twenty minutes were logged somewhere as a small engineering success.
Software can be excellent at meeting its engagement objectives while producing experiences that conflict with what its users wanted from their time. What happens when software succeeds at keeping us engaged but fails to respect our original intentions?
Engagement in itself is not the problem. Reading a long essay, learning a language or a video call with family are engagement too. The useful distinction is between engagement a person would endorse on reflection and engagement they drift into and later regret.
Public conversation tends to land on the individual. In a July 2026 segment of NPR's TED Radio Hour, journalist Nayeema Raza proposed that three pre-internet habits could help people slow down and break the screen cycle. Personal habits matter, but every habit plays out in an environment, and on a screen that environment is written by developers. This essay looks at that side of the glass.
The architecture of engagement
Almost every feature associated with compulsive use began as a fix for a real problem. Infinite scrolling removed the friction of pagination on small touchscreens. Autoplay spares someone watching a series from reaching for the remote. Notifications tell you a person needs you. Recommenders find the few relevant items among millions. Streaks help people keep up practice they chose.
What these features share is that they remove a decision point. A paginated list ends, and the end is a natural moment to ask whether you want more. Each convenience shortens the distance between continuing and never having decided at all.
Reviewing 43 papers, Monge Roffarello, Lukoff and De Russis catalogued eleven types of what they call attention-capture damaging patterns, infinite scroll and autoplay among them. They distinguish deceptive designs, which mislead, from seductive ones, which deceive no one and simply make continuing the path of least resistance. Most engagement features are of the second kind, which is why intent is a poor guide to their effects.
In a survey of 120 YouTube users, Lukoff and colleagues found that autoplay and recommendations primarily undermined people's sense of agency, while search and playlists supported it. That is self-reported perception, not measured harm, but it shows that one product can contain features that hand control to the user and features that quietly take it back.
Underneath the interface sits the objective function. A ranking model trained on watch time or click-through rate learns to serve whatever produces those signals. Kleinberg, Mullainathan and Raghavan formalized the difficulty in a theoretical model in which users have an impulsive in-the-moment self and a long-term self with different preferences, while the platform observes only behavior. In their model, raising engagement improves welfare along some directions of design change and lowers it along others, and engagement data alone cannot reveal which. It is a model rather than an empirical finding, but it names the core problem: behavioral data measures what people do, not what they wanted.
What behavioral science actually tells us
The science here is more interesting and less alarming than the popular version.
Habits. Wood and Rünger's review describes habits as responses that, through repetition in stable contexts, come to be triggered by cues rather than deliberate goals. They are an efficient default mode of action, with a curious side effect: people tend to infer from how often they do something that they must have intended it. Software that sits one tap away in every idle moment is well positioned to become such a cue.
Uncertain rewards. Since Skinner's operant conditioning research, psychologists have known that behavior rewarded intermittently tends to persist longer than behavior rewarded every time. A feed mixes things you care about with things you do not, in an unpredictable order, which is why HCI researchers, Rixen and colleagues among them, have discussed partial reinforcement as a plausible explanation for scrolling's pull. It is a plausible hypothesis built on an analogy to laboratory findings, not an established mechanism of feed use.
Dopamine and the word "reward." In 1997, Schultz, Dayan and Montague showed that the activity of dopamine neurons in monkeys matched a reward prediction error, the quantity at the heart of temporal-difference learning algorithms. That paper bridged machine learning and neuroscience, and it has been stretched far beyond what it shows. The signal it describes concerns learning, the updating of expectations, rather than pleasure, and nothing in those recordings establishes that every notification releases dopamine in your brain.
The shared vocabulary causes its own confusion. In a recommender trained with reinforcement learning, "reward" is a number an engineer chose, such as a click or seconds watched, and "learning" means updating parameters. In psychology and neuroscience, the same words describe how organisms' behavior and neural activity change with experience. The mathematics overlaps; the mechanisms differ. What genuinely connects them is the behavioral loop: the system learns what you respond to while you learn what the system delivers. Two learners, coupled through an interface.
Attention. The experimental evidence on interruptions is clearer. In a laboratory study, Stothart, Mitchum and Yehnert found that merely receiving a call or text notification, without answering it, impaired performance on an attention-demanding task. In a randomized field experiment with 237 smartphone users, Fitz and colleagues found that people whose notifications were batched three times a day felt more attentive, productive and in control, and reported better mood, than controls. Hourly batching did little, and no notifications at all brought more anxiety and fear of missing out. The lesson concerns timing and dose, not abolition.
Self-regulation. In an experience-sampling study of 205 adults, Hofmann, Vohs and Baumeister found that desires for media use, along with work, brought about the most self-control failure. A randomized experiment by Allcott, Gentzkow and Song offers stronger evidence. Temporarily paying people to use social media less reduced their use even after the payments stopped, consistent with habit formation, and letting people set limits on their own future use reduced it substantially, suggesting they wanted to use less than they did. Through their economic model, the authors estimate that self-control problems account for 31 percent of social media use: a model-based estimate, but resting on an experimental pattern that is hard to dismiss.
The trouble with "addiction." None of this licenses calling ordinary use an addiction. Reviewing the literature, Panova and Carbonell found insufficient support for smartphone addiction as a disorder and proposed "problematic use" instead. Kardefelt-Winther and colleagues warned that the behavioral-addiction label is being stretched until it pathologizes ordinary leisure.
"Screen time" deserves similar caution. Across three large datasets, Orben and Przybylski found that digital technology use explained at most 0.4 percent of the variation in adolescent well-being; Twenge, Haidt and colleagues contested that reading, and the debate remains open. Meanwhile, a meta-analysis by Parry and colleagues found that self-reported media use correlates only moderately with logged use, and problematic-use scales even more weakly. Total minutes is a blunt instrument. What people do, why, and whether it crowds out the rest of their lives tell us far more.
Why willpower is only part of the equation
If behavior were purely a matter of resolve, interface design would be irrelevant. The Fitz study is a quiet rebuttal: participants did not become more disciplined; their environment changed, and their experience changed with it.
A useful way to see this is to count actions. In a continuous feed, continuing costs nothing, while stopping requires a decision nothing in the interface prompts. In a batched design, continuing costs one deliberate action and stopping costs none. Neither forces anything; they differ in which choice is the default.
A field study by Rixen and colleagues shows how rarely the app itself provides the exit. Tracking 46 participants' social media sessions for a week, they found that most reasons for ending a session had nothing to do with the app: a waiting task, sleep, food, another person. Sessions spent mainly scrolling were rated less positively than those spent mainly posting or watching stories, though individual differences mattered far more than session content.
It is tempting to conclude that friction is the answer. The evidence is more modest. A large meta-analysis of choice-architecture interventions by Mertens and colleagues, subsequently corrected by its authors, reported an average effect, but a reanalysis by Maier and colleagues found no evidence of an overall effect after correcting for publication bias, a disagreement still unresolved. In HCI, Monge Roffarello and De Russis's meta-analysis of digital self-control tools reported a small-to-medium effect on reducing time spent, but noted that the evidence comes mostly from short experiments, often without control groups, that cannot show whether the change lasts. Friction is a design variable worth testing, not a cure.
Two ways to build a content feed
To make the abstraction concrete, here are two feeds side by side in a single HTML file, with no dependencies, framework or network calls. Save it as two-feeds.html and open it in any modern browser.
Version A (continuous) uses an IntersectionObserver on a sentinel element below the list. When the sentinel comes within 200 pixels of the visible area, the next batch loads automatically.
Version B (intentional) shows ten posts, then a clear stopping point: a summary of what you've seen and a button you must press to load more.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Two Ways to Build a Feed</title> <style> body { font-family: system-ui, sans-serif; margin: 0; padding: 1rem; background: #f6f6f4; color: #1d1d1b; } main { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } section { background: #fff; border: 1px solid #ddd; border-radius: 8px; display: flex; flex-direction: column; } h2 { font-size: 1rem; margin: 0; padding: .75rem 1rem; border-bottom: 1px solid #ddd; } .scroller { height: 70vh; overflow-y: auto; padding: 0 1rem; } article { padding: .75rem 0; border-bottom: 1px solid #eee; } article h3 { font-size: .95rem; margin: 0 0 .25rem; } article p { margin: 0; font-size: .875rem; color: #555; } .status { font-size: .8rem; color: #666; padding: .5rem 0 1rem; min-height: 1.2em; } .stop { text-align: center; padding: 1.5rem 0; } button { font: inherit; padding: .5rem 1rem; border-radius: 6px; border: 1px solid #1d1d1b; background: #fff; cursor: pointer; } button:disabled { opacity: .5; cursor: progress; } button:focus-visible { outline: 3px solid #3366cc; outline-offset: 2px; } </style> </head> <body> <main> <section aria-labelledby="a-title"> <h2 id="a-title">Version A: continuous</h2> <div class="scroller" id="a-scroller"> <div role="feed" id="a-feed" aria-busy="false" aria-labelledby="a-title"></div> <div id="a-sentinel" aria-hidden="true" style="height:1px"></div> <p class="status" id="a-status" role="status"></p> <div class="stop" id="a-error" hidden> <button type="button" id="a-retry">Try again</button> </div> </div> </section> <section aria-labelledby="b-title"> <h2 id="b-title">Version B: intentional</h2> <div class="scroller" id="b-scroller"> <div role="feed" id="b-feed" aria-busy="false" aria-labelledby="b-title"></div> <div class="stop" id="b-stop" hidden> <p id="b-summary"></p> <button type="button" id="b-more">Load 10 more</button> </div> <p class="status" id="b-status" role="status"></p> </div> </section> </main> <script> // ---- Shared mock data source (no network) ---- const BATCH = 10; function fetchPosts(offset, count) { return new Promise(resolve => { setTimeout(() => { const posts = Array.from({ length: count }, (_, i) => { const n = offset + i + 1; return { id: n, title: `Post #${n}`, body: `Mock content for item ${n}.` }; }); resolve(posts); }, 300); // simulated latency }); } function renderPosts(feedEl, posts) { const frag = document.createDocumentFragment(); for (const post of posts) { const el = document.createElement('article'); el.tabIndex = 0; el.setAttribute('aria-posinset', String(post.id)); el.setAttribute('aria-setsize', '-1'); // total unknown el.innerHTML = `<h3></h3><p></p>`; el.querySelector('h3').textContent = post.title; el.querySelector('p').textContent = post.body; frag.appendChild(el); } feedEl.appendChild(frag); } // ---- Version A: loads automatically near the bottom ---- function createContinuousFeed() { const scroller = document.getElementById('a-scroller'); const feed = document.getElementById('a-feed'); const sentinel = document.getElementById('a-sentinel'); const status = document.getElementById('a-status'); const errorBox = document.getElementById('a-error'); const retry = document.getElementById('a-retry'); let loaded = 0; let loading = false; let destroyed = false; async function loadMore() { if (destroyed || loading) return; loading = true; const retryHadFocus = document.activeElement === retry; retry.disabled = true; feed.setAttribute('aria-busy', 'true'); status.textContent = 'Loading…'; let posts; try { posts = await fetchPosts(loaded, BATCH); } catch (err) { if (destroyed) return; // feed was torn down while waiting feed.setAttribute('aria-busy', 'false'); status.textContent = "Couldn't load more posts."; errorBox.hidden = false; // explicit retry, never an automatic loop retry.disabled = false; loading = false; return; } if (destroyed) return; // ignore a response that arrives after teardown renderPosts(feed, posts); errorBox.hidden = true; retry.disabled = false; // The retry button disappears, so hand keyboard focus to the first new post. if (retryHadFocus) feed.children[loaded].focus(); loaded += posts.length; feed.setAttribute('aria-busy', 'false'); status.textContent = ''; loading = false; // An observer only reports *changes*. If the sentinel is still // visible after rendering (tall screen), re-observe to trigger a fresh check. observer.unobserve(sentinel); observer.observe(sentinel); } const observer = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting)) loadMore(); }, { root: scroller, rootMargin: '200px' }); observer.observe(sentinel); retry.addEventListener('click', loadMore); return { destroy() { destroyed = true; observer.disconnect(); retry.removeEventListener('click', loadMore); } }; } // ---- Version B: a finite batch and an explicit decision ---- function createIntentionalFeed() { const feed = document.getElementById('b-feed'); const stop = document.getElementById('b-stop'); const summary = document.getElementById('b-summary'); const button = document.getElementById('b-more'); const status = document.getElementById('b-status'); let loaded = 0; let destroyed = false; async function loadBatch() { button.disabled = true; // prevents double-clicks firing two requests feed.setAttribute('aria-busy', 'true'); status.textContent = 'Loading…'; let posts; try { posts = await fetchPosts(loaded, BATCH); } catch (err) { if (destroyed) return; // feed was torn down while waiting feed.setAttribute('aria-busy', 'false'); status.textContent = "Couldn't load posts."; button.textContent = 'Try again'; stop.hidden = false; button.disabled = false; return; } if (destroyed) return; // ignore a response that arrives after teardown renderPosts(feed, posts); loaded += posts.length; feed.setAttribute('aria-busy', 'false'); summary.textContent = `You've reached the end of this batch (${loaded} posts so far).`; status.textContent = `${posts.length} posts loaded.`; button.textContent = 'Load 10 more'; stop.hidden = false; button.disabled = false; } button.addEventListener('click', loadBatch); loadBatch(); return { destroy() { destroyed = true; button.removeEventListener('click', loadBatch); } }; } const feedA = createContinuousFeed(); const feedB = createIntentionalFeed(); </script> </body> </html>``` The engineering differences are small. Version A needs a guard flag so a fast scroll can't trigger overlapping requests, plus one subtle line: an IntersectionObserver reports only changes in visibility, so if a new batch is too short to push the sentinel off-screen, nothing would fire again. Unobserving and re-observing forces a fresh check. Version B prevents duplicate requests by disabling its button while loading. Both mark the feed aria-busy during loads and announce status through a live region. Teardown needs one more guard. Calling destroy() disconnects the observer or removes the listener, but a request may already be in flight, so each feed also sets a destroyed flag and ignores any response that arrives afterward. With a real network call, you would also pass an AbortController signal to fetch to cancel the request itself; the mock cannot be cancelled, so the flag does that job here. The mock never fails, but a real source will, so both feeds catch errors and show a "Try again" button rather than retrying automatically, which keeps a failing source from trapping the page in a retry loop. The behavioral difference is the one described earlier. In Version A, reaching the end of the content is never an event, so the decision to stop must come entirely from the person. In Version B, the end of a batch becomes a visible moment, along with a count of what has been seen, and continuing becomes a choice rather than a default. What the demo does not show matters as much. It illustrates two design choices. It is not evidence that Version B improves well-being, reduces regret or changes habits. Those are empirical questions, best answered by testing with your own users. **Can we measure success differently?** Teams optimize what they measure. Time spent, session length and daily active users are cheap to collect, hard to dispute and tied to revenue. As the Kleinberg model makes explicit, they cannot distinguish a person who stayed because the product was valuable from one who stayed because leaving took effort. Complementary signals get closer. Task completion asks whether someone who arrived to do something did it. Satisfaction and perceived control, gathered in brief in-product surveys, capture whether the time felt worthwhile. Notification usefulness shows in how often notifications are opened deliberately rather than dismissed or disabled. Voluntary return, sessions started without a prompt, separates a pull the user generates from one the system generates. Reported regret asks directly whether a session was time well spent. None of these is easy. Surveys suffer from low response rates and selection bias, and regret is colored by mood. Users genuinely disagree: one person's relaxing evening of scrolling is another's lost one. And any metric that becomes a target can be gamed, a well-being metric included. The practical approach is to treat these as guardrails, not replacements. When an experiment raises session length, check task completion and reported control in the s...

