Your Prototype Is Not Being Honest With Your Users (And Here’s How To Fix It)
There’s a moment in almost every usability session where a participant pauses at the login screen, types something, and glances up: checking whether they’re “doing it right.” That pause is a clear sign. They’ve already clocked that this isn’t a real app, and every data point collected after that moment is filtered through that awareness. This article has been kindly supported by our dear friends at ProtoPie who help users turn their ideas into highly interactive animations and prototypes for mobile, desktop, web, and IoT. Thank you! There’s a moment in almost every usability session where a participant pauses at the login screen, types something, and glances up: checking whether they’re “doing it right.” That pause is a clear sign. They’ve already clocked that this isn’t a real app, and every data point collected after that moment is filtered through that awareness. In financial product testing, the problem is sharper. Finance users are trained to notice when something feels off: a balance that doesn’t add up, a field that accepts anything. When a banking prototype skips real authentication, participants don’t just disengage; they stop mid-session to flag it. The team walks away with findings that reflect how users behave in a demonstration, not in a real product. The fix is narrower than you’d think. Identify the moment where participant trust is established and make that interaction real. In a banking app, that moment is the login. This tutorial builds it: credentials that validate, a live error state, and a biometric animation that feels native — no code required.
What We’re Building
A Login That Behaves Like A Shipped Product The login flow, built around Pie Bank, a mobile banking prototype, includes functional text inputs, a masked password field, credential validation, a live error state, and a Face ID animation timed to feel indistinguishable from iOS.
What you’ll need
A login UI from Figma (or any supported design tool) ProtoPie Studio — free to start , everything in this tutorial works on the free plan A Lottie file for the Face ID animation ( this one is what we used) The finished Pie Bank prototype file — download it to follow alongside, or use it as a reference after you build
Step 1: Import From Figma Choose Scene, Not Flattened In Figma
open the ProtoPie plugin with your login frame selected and choose Scene when exporting. Flattened collapses everything into a single image; Scene preserves your layer hierarchy so every element arrives in ProtoPie as a separate, targetable layer. Before moving on: rename every layer meaningfully. “Input Username” not “Rectangle 14”. You’ll reference these names in formulas: vague names compound into real time lost.
Step 2: Swap Static Fields For Inputs That Actually Accept Text ProtoPie’s native
Input layer accepts real keyboard entry: participants type actual text, not tap a placeholder. Go to Text → Input , drag an Input layer onto your canvas, and nest it inside your username field group. Match it visually: placeholder text Username , background fill and font to match your design. Hit preview. Click the field. Type. That’s the prototype starting to behave like an app rather than depicting one. Rename this layer Input Username , duplicate it, and nest the copy inside your password field group.
Step 3: One Property Change Masks The Password On the duplicated layer
change placeholder text to Password and set Type to Text Password . ProtoPie handles the masking: no custom logic needed. Preview both fields: username shows text, password shows dots. It already feels real, and you haven’t written a single condition.
Step 4: Build The Destination Scene Before Wiring Navigation Add a new scene, even a blank one
The most common sequencing mistake in ProtoPie is trying to wire a navigation response before a destination exists. Create it first.
Step 5: Wire The Button: It Works, But It Still Lets Everyone Through Select Log
In , add a Tap trigger, set response to Jump , target your dashboard scene, transition Slide in from right to left . Preview and tap. It navigates: but for any input, including nothing. The prototype is still lying. The next two steps are the fix.
Step 6: Add Variables So The Prototype Remembers What Was Typed At the bottom-left
of ProtoPie, add two Text type variables: username and password . Bind each to its input layer with a formula: input(“Input Username”).text input(“Input Password”).text Enable the debug icons: green overlays will show live variable values as you type. When you see your keystrokes appear in real time, the binding is confirmed.
Step 7: Add A Condition So Only Valid Credentials Get Through Go back to the Tap
trigger on the login button. Add a Condition with two rules, both must be true: username equals alex.c@gmail.com password equals ABC123 Move the Jump response inside this condition. Wrong credentials, empty fields, wrong format: none get through. Participants now have to actually log in. That single constraint changes the texture of every test session that follows.
Step 8: Build The Error State, The Interaction Most Prototypes Skip Find your error
message layer, rename it Error Text , set initial opacity to 0 . Add a second condition (the inverse of the first), and inside it, a Change Property response setting Error Text opacity to 100 . Wrong credentials: error appears. Correct credentials: dashboard. Two outcomes: which is what makes this testable, not just demonstrable.
Step 9: Add the Face ID Animation, The Detail That Makes Testers Ask “Is This Real”
Go to Media , drag a Lottie layer onto canvas, load your Face ID file, and position it off-screen above the iPhone frame. On your Login with Face ID button, add a Tap trigger (rename it Tap Face ID ) with four responses in sequence: Move : Lottie container to Y: 60 Playback : Seek: time 0s (resets so it always plays from the start) Playback: Play : Lottie file Jump : to dashboard
Step 10: Stagger The Timing, This Is What Makes It Feel Native Without delays
all four responses fire at once and the scene jumps before the animation plays. Add offsets: Response Delay Move 0s Seek 0s Play 0.5s Jump 1s Enable Reset selected scenes on Jump: without it, navigating back leaves the animation stuck at Y: 60 . Preview: tap Face ID, animation drops in, plays, screen transitions. A biometric login indistinguishable from the real thing. You can download Pie Bank, Chapter 1: Login Flow and explore it freely. A Login This Real Changes What You Can Learn From Your Prototype When authentication actually works, the error state becomes a genuine research touchpoint: do users understand the message, do they retry, do they reach for Face ID instead? These are questions a faked login can’t answer. In stakeholder reviews, the flow speaks for itself. In engineering handoff, the interaction panel documents the behavior (conditional logic, variable bindings, timing) so engineers see intent, not interpretation. This is why FinTech teams invest in login fidelity even when the login isn’t the feature being tested. It’s where participant trust is established. Get it right, and everything downstream produces better signal. This tutorial is part of the FinTech Prototyping with ProtoPie series on the ProtoPie blog. The series builds Pie Bank from the ground up across four chapters, covering the dashboard, money transfer logic, and camera integration. If this tutorial was useful, the rest of the series goes further.
Four Levels Of Customer Understanding
What people say, feel, think, and do are often very different things. To understand the underlying reasons for user behavior, it helps to look beyond the surface and explore hidden motivations, root causes, and the different layers of reality that shape how people act. Brought to you by Measuring UX Impact , friendly video course on UX and design patterns by Vitaly. Many companies think they know fairly well what their users want and need, and how they make their decisions. Yet most of the time these are merely big assumptions and big hunches — with little real evidence to support them. In practice, obvious reasons might be true, but they rarely paint the full picture. To understand our customers, we must triangulate across four levels of customer understanding by Hannah Shamji. It’s a useful way to think about the underlying reasons for user behavior, hidden motivations, and the complex layers of messy and noisy reality that are often overlooked. Let’s see how it works. Four levels of customer understanding: what people say, think or feel, do, and why they do it. By Hannah Shamji .
Don’t Ask Users Your Burning Questions
To learn about customers, it might seem reasonable to ask people what they think and draw conclusions from it. But it’s rarely an effective way to get actionable answers. In fact, as it turns out, what people think , feel , say , and do are often very different things . People don’t always cancel because they want to. Reasons for voluntary and involuntary customer churn. By Emily Anderson . As Erika Hall wrote , asking a question directly is the worst way to get a true and useful answer to that question. We don’t always understand or are aware of our true motivations . We often apply our own context and interpretations to questions. We also exaggerate ( a lot! ). We focus on edge cases and unrealistic scenarios, and we favor short-term goals over long-term goals. So if users say that they absolutely need to compare products in a table , it doesn’t mean that they couldn’t get to their underlying goal without it .
“Possible” vs. “Probable”
Just to indicate how tricky listening to words alone is: even little nuances in words chosen matter. In practice, users are rarely precise in expressing their thoughts, and a good example is the distinction between possible , plausible , and probable , as discovered by Thomas D’hooge . Numerical interpretation of probability phrases and their ranges. Source . A study on Dutch verbal probability terms shows how unreliable the choice of words is. While extreme words have some agreement, terms like “possible,” “maybe,” “uncertain,” or “likely” lead to a wide spread of interpretations . So we shouldn’t rely on what people say, but rather try to go deeper.
The Levels Of Understanding
To get a more realistic and less biased view of customers’ needs, we need to understand a broader picture across 4 levels: Four levels of customer understanding: what people say, think or feel, do, and why they do it. By Hannah Shamji, visualized by Helio.
Level 1: “What they say”
Easier to collect, but mostly opinions, and most unreliable. People often explain their behavior through the lens of how they perceive it, or how they want it to be perceived , which isn’t always accurate. We shouldn’t rely too much on CRM data, surveys, or polls.
Level 2: “What they think and feel”
Gives more context, but is still heavily shaped by memory and personal preferences. Good user research and interviews help us understand expectations and experiences.
Level 3: “What they do”
We study actual behavior, actions taken or skipped, usage data, and analytics. We run task analysis and workflow analysis to understand how people use the product.
Level 4: “Why they do it”
We study underlying motivations and root causes, through observations of real workflows and in-depth interviews. Typically, it requires a trustworthy relationship with the user, repeat interviews, and task walkthroughs. Personally, I wouldn’t recommend NPS ( alternative ). It’s worth noting that different levels might reveal conflicting or contradictory data. To get a better understanding, we need to triangulate and reconcile data with mixed-method research.
Capturing Emotions And Nuance
Emotions are always difficult to capture, but they are easier to spot once you observe people doing what they need to do without external influence or interruptions. The ability to positively impact users grows by moving from sympathy to empathy or even compassion, as articulated by Sarah Gibbons. Spectrum of Empathy: from pity to sympathy to empathy to compassion. By Sarah Gibbons . In the past, I was using “speak-aloud” protocol and asked users to walk me through their thought process as they were completing tasks. But it actually turns out to be quite disruptive. Because people are focused on speaking at the same time while solving a task, many emotions remain hidden or obscured by their language. So, when conducting usability testing, I don’t ask users to speak through their experience. Instead, I observe where they tap or hover with the mouse, where their mouse circles without an action, where they scroll, and how long. Eventually, when a user confirms that they are done or that they are stuck, I ask questions. The Wheel of Emotions helps articulate emotions more precisely. The Emotion Wheel ( website ) by Geoffrey Roberts is a helpful little tool for better describing a range of emotions during user interviews or design sessions. It certainly needs refinement for product design needs , but it helps us get more precise about the sentiment customers or colleagues might be experiencing, moving beyond just “good” or “bad”. One helpful trick is to use mirroring — repeating what a user has said, or ask the same question twice, just paraphrasing it. Or navigating the emotions wheel (see above) to better capture and understand the emotion. These strategies help uncover some of the issues that perhaps didn’t come up in the first answer. That’s also when a user tends to add more useful context and details as they explain their confusion.
Emotions Aren’t Everything
Some people strongly disagree : “Our work is about others — their problems, their pain, their mess. Our job is to make sense of it and then do something about it. Not to emote or perform but to act on and solve it. There is a flawed belief that to build great things, you first need to emotionally fully absorb someone else’s experience.” — Alin Buda Different solutions can cause different levels of harm, which can be way more severe and impactful than emotional response. By Indi Young . I think that Alin brings up a very strong argument, and personally, I find it difficult to disagree with. However, I do see user’s emotional response as a signal of how well the product is working for them. How engaged or detached they are in their journey, how they react to aesthetics , how confused or confident they are. Ultimately, these are signals. To make a difference, we must go beyond emotions and explore what people actually do. Usually, this means relentlessly observing, diagnosing, and focusing on underlying user needs.
Observe And Diagnose, Don’t Validate
Instead of asking, we need to observe . Usually, I focus on small things that make or break an experience. I see where users lose time , repeat actions, hover without clicking, or click and then go back. Pay attention to subtle cues like scratching their neck, raising eyebrows, or expressions of worry, joy, or confusion. Many companies talk about “validation” through user testing, but often that means simply confirming existing assumptions. But we should instead diagnose existing behavior without preconceived notions or affiliations. We don’t validate — we actually research instead. Words to use instead of “validate”: research, understand, investigate, assess, evaluate, examine, learn. By Nikki Anderson . That research means not just understanding customers’ real motivations , but also risks, doubts, concerns, worries, and perhaps even harms . The only way to get there is by building a sincere, honest, and trustworthy relationship — one that feels right and resonates deeply. When customers truly care and want to help, getting to a real understanding becomes much, much easier.
Practical Ways To Uncover User Needs
We don’t need expensive tools to uncover user needs. David Travis provides a fantastic overview of helpful strategies to do just that. Here are some initiatives to spread the word about real user’s struggles or gain a deeper understanding of user needs: Exposure hours , when every employee must be exposed to their customers for at least 2 hours every 6–12 weeks. Live UX testing , where we invite everyone in the company to join and observe. Co-design with users , where we show new features and ask users to rank them. Helpdesk insights , where we ask for frequent complaints and questions from the support every 3–6 months. Listening in , where we tune in on a customer service call, web chat, or eavesdrop where users hang out. The core idea here is that you don’t need extensive and expensive tools to uncover user needs. You need to create spaces where customers’ struggles can be exposed and make these struggles visible across the entire company. It can be short video clips of user sessions or a monthly newsletter with what we learned this month. Making these pain points visible can rally everyone from marketing to engineering to keep users’ struggles at the back of their minds.
Wrapping Up
To make an impact, we must go way beyond user feedback . It’s never enough to listen to surveys — we must observe customers’ actual behaviors and build relationships to truly understand their goals and their motivations. And most importantly, we need to understand what questions we actually want to have answered. Not what “validation” we need to move on with the project, but what we don’t know and what we need to research . Without it, everything else is merely hunches and assumptions — and often wrong and expensive ones. Meet “Measure UX & Design Impact” Meet Measure UX & Design Impact , Vitaly’s practical guide for designers and UX leads on how to track and visualize the incredible impact of your UX work on business — with a live UX training later this year. Jump to details . Meet Measure UX and Design Impact , a practical video course for designers and UX leads. Video + UX Training Video only Video + UX Training $ 495 .00 $ 799 .00 Get Video + UX Training 25 video lessons (8h) + . 100 days money-back-guarantee. Video only $ 250 .00 $ 350 .00 Get the video course 25 video lessons (8h). Updated yearly. Also available as a UX Bundle with 3 video courses.
Useful Resources
Four Levels of Customer Understanding , by Hannah Shamji 60 Ways To Understand User Needs , by David Travis Emotion Wheel Toolkit (PNG) , by Geoffrey Roberts Feelings Wheel PDF Feelings Wheel Online My Case Against Empathy , by Alin Buda Possible vs. Probable , by Thomas D’hooge Communicating probability: a multinational study of the interpretation of verbal probability terms , by Maarten C. de Vries, Marjolijn L. de Boer, and Martine Bouman.
Useful Books
Deploy Empathy: A practical guide to interviewing customers , by Michele Hansen Humankind , by Rutger Bregman
Advanced Tree Counting: Mathematical Layouts With sibling-index() And sibling-count()
Meet sibling-index() and sibling-count() . Staggered cascade effect in one line of CSS without :nth-child() rules or JS workarounds. Works for 5 items or 5,000. You know that thing where you have a grid of cards, and you want them to fade in one after another? That staggered cascade effect. Looks great. Should be simple. And yet every time I’ve built it, the implementation has made me feel like I’m doing something fundamentally stupid. See the Pen [Dynamic Staggered Animations with CSS sibling-index() [forked]](https://codepen.io/smashingmag/pen/zxowBog) by Durgesh . See the Pen Dynamic Staggered Animations with CSS sibling-index() [forked] by Durgesh . Because the options were always the same. Say you want staggered animation delays on a list of 10 items. You either wrote a Sass loop that spat out a dozen :nth-child() rules, each one hardcoding a –index variable for that specific position: / One rule per item. Hope the list never grows. / li:nth-child(1) { –idx: 1; } li:nth-child(2) { –idx: 2; } li:nth-child(3) { –idx: 3; } / … eight more of these … / li:nth-child(10) { –idx: 10; }
li { animation-delay: calc(var(–idx) 100ms); } Ten items. Ten rules. If the list grows to 50? You cap it and hope for the best, or set up a Sass loop that generates hundreds of selectors at build time. Engineers like Roman Komarov have come up with O(√N) strategies — legitimately clever stuff — but you still end up with 63 rules to cover 1,023 elements. Or you looped through elements in JavaScript and set inline styles. style=”–index: 3″ . Right there in the DOM. Works fine. Also spreads layout concerns across your scripts and quietly breaks six months later when someone refactors the component without realizing the CSS depends on a JavaScript-injected variable. Both approaches have always bugged me for the same reason: you’re telling the browser something it already knows . The browser built the DOM tree. It knows which element is the third child. It has the data. CSS just couldn’t access it. Well, now it can: li { animation-delay: calc(sibling-index() 100ms); } One line. Works for 5 items or 5,000. No event listeners. No mutation observers. No re-renders. sibling-index() and sibling-count() are part of the CSS Values and Units Module Level 5 spec (Section 9, if you’re the type who reads W3C drafts for fun). The proposal was approved via CSSWG issue #4559 after substantial discussion. The functions themselves take no arguments — you just use them. sibling-index() gives you the 1-based position of an element among its parent’s children. First child returns 1 . Fifth child returns 5 . It only counts element nodes — text nodes, comments, and whitespace are all invisible to it. sibling-count() gives you the total number of element children the parent has. Basically, the CSS equivalent of element.parentElement.children.length in JavaScript, but available in your stylesheet. Both functions resolve to
Patterns Worth Stealing
Once it clicks that these are just integers, ideas come fast.
Reverse Stagger
Want the last item to animate first? Subtract: .card { animation: fade-in 0.4s ease both; animation-delay: calc((sibling-count() – sibling-index()) 80ms); } Last child gets (N – N) 80ms = 0ms — it fires instantly. First child gets (N – 1) 80ms . The animation kicks off the moment the page loads instead of pausing for an awkward beat.
Automatic Equal Widths
Stop counting children manually to set percentages: .tab { width: calc(100% / sibling-count()); } Five tabs? 20% each. Add a sixth? 16.66%. Remove two? 25%. No media queries, no resize observers, no JavaScript at all. That said, you can imagine a scenario where too many items make for really narrow tabs, at which point you might want to go with something else, perhaps a Flexbox wrapping solution.
Hue Distribution
Spread colors evenly across the color wheel: .swatch { background-color: hsl( calc((360deg / sibling-count()) sibling-index()) 70% 50% ); } Three items get hues 120° apart. Twelve items get 30° increments. The palette adapts to whatever’s in the DOM, which is the kind of thing you’d normally reach for a JavaScript color library to do.
Circular Menus
Distributing items in a circle used to mean calculating sine and cosine in JavaScript. CSS now has sin() and cos() natively (Juan Diego RodrÃguez has a great practical walkthrough of these on CSS-Tricks), and combined with tree-counting, the whole thing collapses into pure CSS: .radial-item { –angle: calc((360deg / sibling-count()) * sibling-index()); –radius: 120px;
position: absolute; left: calc(50% + var(–radius) cos(var(–angle))); top: calc(50% + var(–radius) sin(var(–angle))); transform: rotate(calc(var(–angle) -1)); } Six items? Hexagon. Eight? Octagon. Add or remove items, and the layout recalculates. No JavaScript computing coordinates.
Z-Index Stacking
Building a card fan? One line: .card { z-index: calc(sibling-count() – sibling-index()); } First card stacks highest, last card gets 0. Flip the math if you want the reverse.
The Gotchas
These are worth going through individually because they’re not obvious from the spec.
Shadow DOM Scoping
sibling-index() and sibling-count() operate on the DOM tree, not the flattened visual tree. This distinction will absolutely bite you with Web Components . Say you have a custom element with this shadow DOM:
If you style .internal with sibling-index() , it returns 2 . Always. Even if the
Pseudo-Elements Don’t Count
before and ::after aren’t siblings. They don’t show up in sibling-count() and they don’t have their own sibling-index() . But — and this is the part that’ll save you a debugging session — you can use these functions inside pseudo-element declarations. When you write #target::before { width: calc(sibling-index() 10px); } , it evaluates sibling-index() against #target , not against the pseudo-element. The pseudo-element isn’t a real node, so the function traces back to its originating element. Same story with ::slotted()::before — it checks the slotted element’s index in the light DOM.
display: none Still Counts
This one burned me. Elements with display: none vanish from the layout tree. They take up no space. Screen readers don’t see them. But they’re still in the DOM. Since sibling-index() reads the DOM tree, not the layout tree, hidden elements get counted:
- Apple
- Cherry
Cherry is 3 , not 2 . The hidden banana still holds its spot. This doesn’t matter for most layouts. But if you’re building something like a search filter that hides non-matching items with display: none , your staggered animations and circular layouts will develop gaps. The visible items keep their original, non-sequential indexes. For anything that depends on continuous counting — radial menus, proportional widths — you’ll need to actually remove filtered nodes from the DOM instead of just hiding them. Or fall back to JavaScript-managed indexes. Note: visibility: hidden and opacity: 0 count too, but that feels more intuitive since those elements still take up space. display: none is the sneaky one because the element disappears visually but still occupies a DOM slot.
Custom Properties Evaluate Immediately
This is subtle. If you try to centralize the index on a parent: .parent { –idx: sibling-index(); } …that –idx resolves right there on .parent . It grabs the parent’s own sibling index, locks it to that number, and every child inherits that single fixed value. Every child gets the same number. Almost certainly not what you want or expect. The fix is simple — put the function on the elements that need it: .child { –idx: sibling-index(); animation-delay: calc(var(–idx) 100ms); } The CSSWG has discussed an inherits: declaration addition to @property that could theoretically fix this. If you haven’t used @property , it lets you define a custom property’s type, initial value, and inheritance behavior — way more control than a raw –variable . But the inherits: declaration idea is still in early CSSWG discussion, not baked into any spec draft. It could be years before it lands — or it might not land at all. Even with @property today, there’s no mechanism to say “don’t evaluate yet, wait for the child.” So for now, just apply directly.
Performance at Scale
Changing the DOM — i.e., adding, removing, reordering children — triggers style recalculation for affected siblings. The browser handles this during the cascade phase (before layout and paint), so it’s faster than the old approach of looping in JavaScript and stamping inline styles. But there’s a real cost if you push it. Inserting an element at the beginning of a container with 10,000 children forces the engine to recalculate the sibling index for all 10,000 elements after it. For normal stuff — navigation, card grids, tab bars — you’ll never notice. For a live stock ticker or an infinite-scroll feed with thousands of nodes constantly churning, keep using JavaScript-managed indexes inside your virtualization window. These functions are fast. They’re not zero-cost.
Browser Support
As of writing, Chrome/Edge 138 shipped these functions in stable releases (June 2025), and Safari 26.2 followed. Firefox hasn’t shipped them in stable yet, but Mozilla’s spec position is positive and implementation work is actively underway — tracked under Bugzilla issue #1953973 . Check caniuse for the latest before you ship. Chrome and Safari together cover roughly 75–80% of global traffic. That’s a strong majority, but Firefox’s absence means you still need a fallback. For shipping today, @supports is your friend: / Baseline that works everywhere / .item { width: 25%; animation-delay: 0ms; }
/ Progressively enhance where supported / @supports (z-index: sibling-index()) { .item { width: calc(100% / sibling-count()); animation-delay: calc(sibling-index() * 80ms); } } Static fallback for Firefox. Mathematical layout for everyone else. Nobody gets a broken page. On polyfills: A JavaScript polyfill that loops through siblings and sets inline styles is the exact thing these functions exist to replace. But that doesn’t mean you’re stuck with hardcoded fallback values either. Juan Diego RodrÃguez wrote a solid piece on “ How to Wait for the sibling-count() and sibling-index() Functions ” that lays out the right model for progressive enhancement until native support hits Baseline. His approach uses existing CSS techniques (like Roman Komarov’s counting hacks ) as a bridge rather than a full JavaScript polyfill. Worth reading if you need to ship something production-ready today while Firefox catches up.
Accessibility Notes
This needs saying because it’s easy to get excited and forget: these functions are purely visual . They change how things look. They don’t change what things mean . If you use sibling-index() math to visually reorder a list — via order or grid placement — a screen reader still reads the DOM in source order. Keyboard tab order follows the DOM, too. Visual layout and semantic structure will contradict each other, and that’s an accessibility failure. For interactive components like data grids, radial menus, or custom listboxes that lean on tree-counting for layout, you still need JavaScript to sync ARIA attributes. aria-posinset and aria-setsize have no idea what CSS is calculating. If your CSS says “this is visually item 3 of 7” but ARIA says something different (or nothing), assistive technology users get a broken experience. On the debugging side, recent versions of Chrome DevTools let you inspect computed sibling-index() and sibling-count() values directly in the Elements panel, which helps when the math isn’t doing what you expect.
What’s Coming
The current spec only counts all element siblings. But the CSSWG has documented a planned extension in issue #9572 : an of
Ten Data-Backed Truths Of User Experience ROI
Every extra second of friction has a measurable business cost. Carrie Webster shares ten data-backed UX facts that link user experience directly to revenue, retention, and long-term growth. In the high-stakes economy of today, the cost of a friction-heavy interface is no longer just “lost clicks”, but potentially millions in wasted engineering spend and lost business value. As a veteran UX designer who has helped build digital products since the early mobile-first era, I’ve watched business leaders shift from viewing design as a “cosmetic preference” to recognising that user experience is actually the primary engine of business survival. A UX design role is as much about research and analytics as it is about pixels, and I believe that hard data is the only tool powerful enough to bridge the gap between design and the boardroom. Facts don’t just advocate for the user; they prove that UX is a non-negotiable requirement for a healthy bottom line. Even in the rooms where decisions are made, UX is frequently undervalued as a ‘visual’ role. I’ve learned that the most effective way to dismantle this myth is through data . The following ten facts represent the current reality of the digital world. These are not just “design tips”; they are the clinical, data-backed pillars for financial growth in a saturated market. Some of these facts are also commonly used by designers as best practices. For example, I once led a B2C mobile design project, where I was able to strip 1.2 seconds off the mobile load time by reducing and removing some of the visual assets. The result was an immediate 12% lift in completed transactions, proving that in UX, every tenth of a second is a direct lever for revenue. 1. Fixing Issues In The Design Phase Is 100 Times Cheaper One of the most compelling financial arguments for UX is the 1:100 rule . Modern studies, such as from the IBM Systems Institute and Sugue Technologies , show that fixing an error after a product has been developed and launched can be up to 100 times more expensive than fixing it during the initial design and prototyping phase. Think of UX as “engineering insurance.” By the time a developer touches the code, every interaction should have been validated. If you discover a fundamental navigation flaw after launch, you aren’t just paying for the fix ; you’re paying for technical debt, lost developer time, and the revenue lost while users struggle with a broken flow. Graph showing the cost of bug fixing during different phases.
2. Performance Impacts User Experience In the current landscape, performance is the essential foundation of user experience. A beautiful interface is worthless if the user bounces before it renders. The data is uncompromising : 47% of users expect a page to load in two seconds or less , and missing this window is a financial catastrophe. A mere one-second delay can reduce conversions by 20% and satisfaction by 16% , while retail businesses lose an estimated $2.6 billion annually to slow load times. When mobile load time moves from one to three seconds, the bounce rate spikes by 32% , and by the third second, conversion rates typically plummet from 40% to 29% . However, this volatility offers a massive lever for growth. Even a microscopic 0.1-second improvement can lift retail conversions by 8.4% , and travel site conversions by 10.1% . Improving your Largest Contentful Paint (LCP) by 31% — a benchmark 67% of websites achieved as of June 2025 — can drive a direct 8% increase in sales . As a long-time designer, I treat speed as a primary design element. If the site isn’t instantaneous, the design hasn’t just failed — it effectively doesn’t exist. When pages load in one second, conversion rates are about 40%. 3. Your Site Has 50 Milliseconds to Impress Your Customers First impressions are both visceral and aesthetic . Research indicates that users form an opinion about a website’s visual appeal in approximately 50 milliseconds (0.05 seconds). That’s not a lot of time! This split-second “gut-feeling” is a survival mechanism that dictates whether a user stays to explore your value proposition or bounces immediately. In the current market, 94% of first impressions are strictly design related . If your interface feels “off” or dated, users subconsciously project that lack of quality onto your entire product or service. Your content effectively doesn’t exist if your design hasn’t earned the five seconds of attention required to read it. 4. Hick’s Law: The Cost of Overwhelm Stakeholders often think “more options” equals “more value.” Psychology proves the opposite. Hick’s Law states that the time it takes to make a decision increases with the number of options available. Every extra menu item or form field is a “tax” on the user’s brain. As noted by Landbase , top-performing sites now achieve conversion rates exceeding 11%, while average performers struggle below 3%. Those performing well have applied personalization and optimization strategies to simplify the experience. If you want to increase your revenue by tomorrow, find one field to delete from your checkout flow today. Demonstrating complex choices vs simple. 5. White Space Improves Comprehension “White space” is often viewed as wasted real estate by non-designers. In reality, it is a tool for focus. Strategic use of white space can increase a user’s content comprehension by up to 20% . White space prevents “cognitive load” from peaking. By giving the user’s eyes a place to rest, you guide them toward the most important elements, usually your “Buy” or “Sign Up” button. In 2026, as attention spans have dropped to roughly 8 seconds , simplicity is the ultimate luxury and a major driver of engagement. For example, in a fintech dashboard I worked on, analyst users were feeling overwhelmed by a ‘data dump’ layout in some of the dashboard components. I applied more white space around the data to lower their cognitive load. Simply giving the data room to breathe led to a 25% decrease in time-on-task and a significant boost in trial-to-paid conversions. 6. The Power Of “Fake” Progress One of the most surprising psychological hacks in UX is that users will complete a task faster if they believe they have already made progress. This is known as the Goal Gradient Effect . In a classic study, researchers found that a 10-stamp coffee card with two stamps already “pre-filled” was completed significantly faster than an 8-stamp card with zero pre-fills, even though the total spend required was identical. In digital design, showing a progress bar that starts at 15% (simply for creating an account) increases completion rates for onboarding by over 40% . We aren’t just designing screens — we are managing the user’s dopamine and sense of momentum. Increased motivation to reach the target based on current progress. 7. Make Your Content Readable Many stakeholders believe that cramming more text “above the fold” increases value. Data proves the opposite . Proper typography, specifically line spacing (leading) and paragraph width, can increase content comprehension and reading speed by up to 20% . Optimal line height (generally 1.5x the font size) reduces “visual noise,” allowing the brain to process information with less cognitive effort. When users struggle to read your text due to tight spacing or small fonts, their “perceived effort” increases, leading to a higher bounce rate. Legibility is a conversion tool: if it’s hard to read, it’s hard to buy. There are many ways to display more legible text. For example, if line spacing (leading) is too small or the font is too heavy, this also impacts readability. This example demonstrates the difference in readability between a light and a heavy font display. 8. Your Users Only Read 20% of Your Content This truth meshes well with the previous one. Users do not read your website; they scan it . On a typical web page, users read only about 20% to 28% of the text . Because modern users scan in an F-pattern or Spotted pattern , designing for reading is a tactical error. We must design for scanning. This requires the following: Bold headers that narrate the value proposition. Bullet points for key benefits. White space to connect users to key information (discussed in the previous truth). High-contrast call-to-action (CTA) buttons. If your core message is buried in a paragraph, it is invisible to nearly 80% of your audience. 9. Why User Testing With 5 People Is the Magic Number I have heard of companies that waste six-figure budgets on massive user studies with 100 people, only to get buried in noise. The reality is that testing with just 5 users typically uncovers 85% of usability problems . This is a mathematical sweet spot. After the fifth user, you reach the point of diminishing returns — you spend more money to find fewer new bugs. The competitive advantage belongs to small and frequent user testing activities. Test with 5 people, iterate, and test with 5 more. It is the most cost-effective way to build a bulletproof product. Personally, I have followed this guideline many times during user testing activities, and I can confidently say that testing with 5 people does deliver the majority of issues in your design. 10. The Financial ROI of 9,900% Last, but definitely not least, the most staggering statistic in our industry remains consistent. On average, every $1 invested in UX returns $100 . This 9,900% ROI isn’t magic, but the sum of increased conversion and reduced support. A fully optimised UX design can improve conversion rates by up to 400% . Furthermore, intuitive design significantly lowers customer support requirements. When a product is self-explanatory, you don’t need a massive call centre to explain how to use it. The Depth of UX Investment Beyond these individual statistics, we must address the cumulative effect of a mature UX practice. In my years of practising, the most successful firms are those that treat UX as a continuous improvement loop rather than a one-off project. The data shows that companies with high design maturity see 32% higher revenue growth and 56% higher total returns to shareholders compared to their less design-focused peers. This discrepancy exists because mature UX organisations move beyond “user delight” and into “user efficiency.” When you shave 30 seconds off a workflow for a team of 1,000 employees, you aren’t just making them happier; you are reclaiming hundreds of thousands of dollars in annual productivity. This internal ROI is often overlooked, but it is just as vital as consumer-facing conversion rates. Furthermore, the “experience gap” is real. 80% of companies believe they deliver a “superior experience,” but only 8% of customers agree. This massive disconnect represents a significant market opportunity for those willing to look at the hard data. By bridging this gap through continuous user testing and performance optimisation, you aren’t just improving a product but capturing market share that your competitors are leaving on the table. The Impact of AI Today, we cannot talk about UX without talking about AI. However, AI hasn’t replaced these 10 facts, but it has accelerated the solution on some of these . Agentic UX 60% of designers are now building “AI agents” that take actions on behalf of the user, drastically reducing the impact of Hick’s Law by narrowing down choices before the user even sees them. Real-Time Personalisation 32% of teams use AI to personalise interfaces in real-time, meaning the F-Pattern scanning habits are catered to by moving the most relevant content to exactly where that specific user’s eyes are likely to land. Automated ROI 93% of designers are using generative AI tools to prototype faster, which brings the 1:100 Cost Ratio even lower by allowing us to find and fix errors before a single line of production code is written. AI has turned UX from a static map into a living, breathing guide for users. But the fundamental rules of human psychology, such as our 50ms judgments and our need for white space, remain unchanged. Conclusion In summary, here is a list of the key truths to remember: Fixing issues in the design phase is 100 times cheaper. Performance impacts user experience. Your site has 50 milliseconds to impress your customers. Hick’s Law: The cost of overwhelm. White space improves comprehension. The power of “fake” progress. Make your content readable. Your users only read 20% of your content. Why user testing with 5 people is the magic number. The financial ROI of 9,900%. As we move deeper into the late 2020s, the line between “design” and “business strategy” has vanished. The data is in, and companies that lead in design outperform their competitors by 1.7x in revenue growth . UX design is no longer a team you hire to “make things look nice.” It is the research-driven, data-backed discipline that ensures your digital product isn’t just a cost centre, but a revenue-generating machine. “ In fact, this has always been the case, but I hope that in presenting these cold, hard truths, it now becomes a reality for your business. As I have found over the years, implementing factual design improvements does make a difference that intuition alone can’t replicate. We are past the era of subjective opinions. The data is clear, the psychology is proven, and the ROI is undeniable. The only question left is whether you’re ready to let the facts lead your design, or if you’ll let your competitors do it first. Further Reading On SmashingMag “ The Human Element: Using Research And Psychology To Elevate Data Storytelling ”, Victor Yocco & Angelica Lo Duca “ AI In UX: Achieve More With Less ”, Paul Boag “ Six Key Components of UX Strategy ”, Vitaly Friedman “ When Friction Is A Good Thing: Designing Sustainable E-Commerce Experiences ”, Anna Rátkai (yk)Tips on front-end & UX, delivered weekly in your inbox. Just the things you can actually use.
Practical Interface Patterns For AI Transparency (Part 2)
Why traditional loading patterns like spinners fail in agentic AI experiences, and how interface patterns that reveal the system’s process, status, and decision-making can improve transparency and build user trust. In the first part of this series , we talked about the Decision Node Audit . We mapped out the internal workings of our AI system to pinpoint the exact moments it makes decisions based on probabilities. This told us when the system needs to be transparent with the user. Now, the big question is how to share that information. You’ve got your Transparency Matrix ready. You know which behind-the-scenes API calls need a visible status update. Your engineers are on board with the technical aspects. The next step is designing the visual container for those updates. We face a legacy problem. For thirty years, interface designers have relied on a single pattern to handle latency: the spinner . The spinning wheel, the throbber, the progress bar. These patterns communicate a specific technical reality. They tell the user that the system is retrieving data. The delay is caused by bandwidth or file size. AI agents introduce a new kind of wait time. When an agent pauses for twenty seconds, it’s not just downloading something; it’s thinking . It’s figuring out the best steps, weighing options, and creating the content you asked for. If we use a basic spinning icon for this “thinking time,” users get confused and anxious. They watch a looping animation and can’t tell if the system is stalled or crashed. They don’t know if the agent is handling a very complicated task or if it has simply failed. To build user trust, we need to turn this waiting time into a moment for reassurance . Instead of a passive “something is happening,” we need to communicate an active, “Here is exactly how I am working to solve your problem.” Writing Clear Status Updates We often think of transparency as a visual design problem, but it’s really about the words we use. Simple, clear explanations (the microcopy) are what build trust and separate a reliable AI from one that feels broken. We need to retire generic placeholders like Loading or Working . These words are remnants of the era of static software. Instead, we must construct our status updates using a specific formula that mirrors the agency of the system. Let’s stop using vague words like “Loading” or “Working.” Those terms belong to the past, when software was simple and static. Instead, we should create status updates that clearly tell the user what the system is actually doing and make the system’s actions transparent. Imagine, for the sake of an example, you are deploying agentic AI that will help team members organize their calendars and plan recurring meetings on their behalf, once prompted. When an AI displays a message like “Checking availability” for an unknown amount of time, users often feel lost because it doesn’t offer enough information. While they understand the AI is looking at a calendar, they don’t know whose calendar it is, what other steps are involved (before or after), or if the AI even remembered the people and purpose of the scheduling request. Waiting for the final result can be a tense, uneasy experience, like anticipating a gift that you suspect might be a prank. Perplexity AI provides a strong example of doing status updates right. Figure 1 below shows that when users ask a question, the interface displays exactly what it is doing in real time. You see a list of activities updating as they are accomplished. Users do not need to guess what is happening as the AI works. Figure 1: Perplexity AI is a good example of showing users the AI’s status, including what terms it is using to search the web.
The Agentic Update Formula To give people useful status updates, we need to connect what the system is doing with why it’s doing it. Keeping with our scheduling agent example, the system should break down that waiting period into at least four clear, separate steps. First, the interface displays Checking your calendar to find open times for a recurring Thursday call with [Name(s)] . Then, it updates to: Cross-checking availability with [Name(s)] calendars . Next, it might display: Syncing [Name(s)] schedules to secure your meeting time on [Data and Time] . Finally, at the conclusion, the agent might state they have successfully completed the task and request the user check their email to confirm the invite that’s been shared with the group having the recurring meeting. This communication process grounds the technical process in the user’s actual life. Making an AI’s progress easy to understand boils down to a three-part structure: a strong Action Word , what the AI is working on (the Specific Item ), and any Limits or rules it has to follow. Think about an AI helping you book a trip. A weak, unhelpful update would just be: Searching for flights… A much better update uses the formula: Action Word: Scanning Specific Item: the prices on Lufthansa and United Limits/Rules: to find anything under $600. This approach clearly shows the user that the AI understood their request and is working within the set boundaries. Figure 2: The Anatomy of an Agentic AI Status Update. Matching Tone to the Risk Matrix Should an AI sound like a person or act like a robot? The right answer depends on the task’s importance, which we can figure out using the Impact/Risk Matrix from our Decision Node Audit . For simple, low-risk tasks, a friendly, conversational tone works best. For example, a scheduling assistant can say it’s checking your calendar for the best time. This creates a comfortable, easygoing experience for the user. However, high-stakes tasks demand clear, mechanical accuracy. If the AI is managing a big financial transfer or a complicated database migration, users don’t want a playful interface; they want precision. A screen that says “I am thinking hard about your money” would possibly cause panic. Instead, the interface should use straightforward language like “Verifying account routing numbers.” By adjusting the AI’s “personality” to match the level of risk, we give users exactly the experience they need in that moment. While the Impact/Risk Matrix provides a necessary starting point, the ultimate determinant of the appropriate AI voice and tone is rigorous user research . It’s impossible for any set of rules to predict the exact words or tone that will build trust or cause stress for every group of users or in every situation. That’s why hands-on research is essential. You need to: Run A/B tests on different ways the AI “talks” to people. Conduct usability studies to see how users react emotionally to the system’s messages. Perform interviews to truly understand what users expect from an AI in terms of openness. This kind of research ensures the AI’s “personality” is comfortable and appropriate for the actual people who will be using the system in their specific context. We’ve now covered the “what” — the critical microcopy, the clear action words, and the necessary limits that make an AI status update honest and informative. But words alone aren’t enough. A perfect sentence hidden in a poor interface is still a failure of transparency. The next challenge is the “how” — designing the physical delivery system for that message. You can think of the status update formula as the engine, and the interface pattern as the car. A powerful engine needs a reliable, well-designed chassis to carry it down the road. Interface Patterns: A Library For Agents Once we have the right words, we need the right container . The key is matching the message’s weight to the pattern’s visibility. A tiny background task (like an agent gently tidying up your files) doesn’t need a loud, flashing banner. That message is best delivered subtly. A high-stakes, multi-step process (like moving money) potentially demands a more robust container that forces the user to pay attention. By creating a library of these patterns, we ensure the right level of transparency is delivered at the right moment, turning the anxiety of waiting into a moment of informed confidence. Let’s review a few common, critical patterns. The Living Breadcrumb: AI Working in the Background For those low-importance tasks that an AI is handling quietly in the background, we need a way to show users it’s working without constantly distracting them. We can call this the living breadcrumb. Think of an email app where an AI is drafting a reply for you. You don’t want a disruptive pop-up message. Instead, a small, subtle status indicator pulses within the application’s border or menu area. The solution needs to go beyond a static icon. The living breadcrumb smoothly transitions between different text updates. It might pulse from Reading email to Drafting reply to Checking tone . It’s there if you want to check on its progress, offering a quiet assurance that the task is underway, but it won’t demand your immediate attention. Figure 3: A subtle, living breadcrumb provides assurance in the background for low-priority tasks, showing the AI is working without demanding constant attention. Dynamic Checklists When dealing with critical, high-stakes tasks — like processing a complex financial transaction or migrating a large, intricate dataset — we recommend using a Dynamic Checklist (illustrated in Figure 3). This pattern serves as a powerful anchor for the user, providing clarity and confidence about the process’s progress . Instead of a simple bar, the Dynamic Checklist lays out every planned step the AI agent will take. It clearly highlights the step that is currently in progress, marks preceding steps as complete, and lists future actions as pending. For example: Step 1 : Verify Account Balance [Complete] . Step 2 : Convert Currency [Processing] . Step 3 : Transfer Funds [Pending] . The Dynamic Checklist offers a significant advantage over a traditional progress bar because it expertly manages unpredictable time. If the currency conversion (Step 2) unexpectedly requires an extra ten seconds, the user won’t feel sudden anxiety or panic. They have full visibility into the system’s exact location, understanding that the delay is occurring during the Converting Currency step. Because they recognize this is a potentially complex action, they are naturally more patient and trusting of the system’s ongoing work. Figure 4: Devin (AI) shows users a dynamic checklist of the tasks it’s accomplished, as well as what remains to be done. The pattern itself is a compelling UI idea, but designers must remember that its implementation transforms the task into a full-stack design requirement. Unlike a simple loading flag, the dynamic checklist requires a robust front-end state management system to listen for step-completion events, which are typically triggered by a back-end webhook structure. This ensures the interface is always reflecting the agent’s real-time position in the workflow. Figure 5: A dynamic checklist is excellent to show transparency in what the AI is doing. It clearly shows the user what steps the AI has already finished, what it’s currently working on, and what it plans to do next. The Thinking Toggle Some users with higher information needs or higher needs for transparency may not trust a simple summary; they want to see the system’s raw processing. For this audience, we’ve designed the Thinking Toggle . This is a simple progressive disclosure UI control, like a chevron or a “View Logs” button, that lets the user expand a friendly status update into a raw terminal view. It displays the sanitized logic logs of the AI agent, such as: Querying API endpoint /v2/search ; Response received: 200 OK ; Filtering results by relevance score > 0.8 . Many people will never open this view. However, for the user who needs deep transparency, the very presence of this toggle is a signal of trust. It reassures them that the system is not concealing anything. Keep in mind, with this deep transparency comes a critical technical risk. Even for your most expert audience, you must sanitize and abstract these raw logs before display. This step is non-negotiable to prevent accidentally exposing proprietary business logic, internal data structure names, or security tokens that could be exploited. This process ensures trust is built through honesty, not security vulnerability. Designing For Partial Success In standard software, things are often black or white. A file either saves or it doesn’t. But with AI agents, things are often grey. An agent might plan most of a trip perfectly, yet struggle to book that one special restaurant. We need to design for when the AI is mostly successful. Standard binary (yes or no) error messages are trust-killers because they suggest the AI failed completely. If an agent does 90% of a task and only misses the last 10%, a big red “Request Failed” banner is misleading. Instead, the interface should clearly show what worked and what didn’t: Flight booked: UA 492 [Success]. Hotel reserved: Marriott Downtown [Success]. Car rental: Hertz [Failed — No inventory]. This way, you only have to step in and fix the parts that failed, like booking the car yourself, while keeping all the good work the agent already did. Figure 6: We need to be more granular in defining how much of a task has been completed when designing content for agentic AI experiences. Disentangling The Tool When an AI system doesn’t perform as expected, it’s crucial to be absolutely clear about the true reason for the failure. Users often mistakenly blame the AI itself for problems that are actually caused by an external service or tool the AI relies on. For example, imagine a virtual assistant tries to look at your schedule, but the connection to the Google Calendar API is down. The error message shouldn’t make the assistant look like it failed to do its job. Less helpful: “I could not check your calendar.” (This suggests the assistant is incompetent.) More helpful and honest: “The Google Calendar connection is not responding. I will automatically try again in 30 seconds.” The first message is frustrating because it makes the AI look like it failed. The second message, though, is much clearer. It explains that the AI is capable, but a broken tool outside its control is causing the issue. This distinction is really important because it keeps the user from losing faith in the AI, even when things go wrong. The Audit Trail: Trust After The Fact Real-time transparency is fleeting. If a user walks away from their desk while the agent is working, they miss the Dynamic Checklist. They return to a finished screen. If the result looks odd, they have no way to verify the work. This is why every agentic workflow requires a persistent Audit Trail. We need to design a Show Work interaction. On the final result screen, provide a link or history log that allows the user to replay the decision logic. See how this price was calculated ; View search sources . This receipt is the ultimate safety net. It allows the user to spot-check the validity of the output. Even if they never click it, the mere presence of the receipt tells the user that the system stands behind its work. Figure 7: The Audit trail design pattern provides the needed transparency to show users how an outcome was achieved. ChatGPT provides an example of how now providing users with an easy way to audit the information AI uses can cause confusion or user frustration. ChatGPT remembers you in the way a file cabinet quietly fills up with notes about everything you’ve ever said, then uses those notes to shape every future conversation without telling you. This is called memory. According to developer Simon Willison , in April 2025, that memory was getting fed into every new conversation automatically. The problem with ChatGPT’s memory at that time was that you couldn’t see what it remembers, or when it’s using that information, or how it’s influencing what you get back. There’s no log. No timeline. No plain-language list of “here’s what the AI has decided about you.” The only way to glimpse the dossier was to know a specific prompt trick — essentially asking the model to quote its own hidden instructions back to you. Most users will never discover this. They’ll just notice, as Willison did, that ChatGPT placed a “Half Moon Bay” sign in the background of an image they generated (Figure 8) because it had silently cross-referenced their location from previous conversations. This is the absence of transparency (the ability to audit the memory with ease) disguised as personalization. You need to provide users with both. Figure 8: Willison asked ChatGPT to put the dog in a pelican costume, but he did not ask for the “Half Moon Bay” sign. Proving that ChatGPT was tracking information on users without clearly allowing users to access what information was being stored. The Audit Trail pattern is the ultimate solution to the memory audit problem demonstrated by ChatGPT. It is one of four core design solutions that, together, create a library of options for improving AI transparency. Here is a quick summary of the key interface patterns discussed in this article, which are designed to transform AI waiting time from a moment of anxiety into an opportunity to build user confidence: Pattern Best Use Case The User’s Anxiety The Trust Signal The Living Breadcrumb Low-stakes, background tasks (e.g., drafting emails, sorting files). Did the system stall or freeze? I am active, but I won’t disturb you. The Dynamic Checklist High-stakes workflows with variable time (e.g., financial transfers, booking travel). Is it stuck? What step is taking so long? I have a plan, and I am currently executing Step 2. The Thinking Toggle Expert tools or complex data analysis (e.g., code generation, market research). Is this hallucinating or using real data? I have nothing to hide; here are my raw logs. The Audit Trail Post-task review for any outcome (e.g., final reports, completed bookings). How do I know this result is accurate? Here is the receipt of my work for you to verify. Table 1: Four design patterns enhancing transparency. The Reality of Attention: When Users Ignore the Interface Even the most perfectly designed checklist or the clearest status message may still go ignored by many users. When people are working on tons of tasks, especially professionals, they often tune out the interface. Think of an insurance underwriter creating fifty quotes a day — they’re not watching a progress bar. They click “Generate,” switch tabs to answer an email, and only come back when the task is done. My research with these experts shows they judge the system based entirely on the final result. They have a good idea of what the answer should be. If a salesperson expects a premium between $500 and $600, and the system returns $550, they accept it right away, and trust is established. These experts tell me that over time, as the AI continues to provide what they perceive as accurate outputs, usage will increase, and they will save time versus manual quoting. Essentially, the system is now viewed as an efficient accelerator of an otherwise monotonous yet mandatory task. But if the system returns $900, the user stops. The output is not aligned with expectations, and that’s a problem they must solve. At that moment, the user switched tabs; they missed the little explanation about the high-risk surcharge that popped up in real-time. They didn’t see the specific rule that was triggered. If that explanation disappeared with the progress bar, the user has no way to understand the difference between expectation and outcome. They certainly won’t run the query again just to watch the animation play out. They will run the quote by hand, effectively treating the AI’s output as useless and initiating a complete rework of their effort. This manual recalculation feels like a waste of time, which further erodes their confidence in the tool. Once this happens, the user is not interested in why the system chose $900; they are focused purely on validating or invalidating the system’s accuracy against their own, trusted methods. This lack of transparency, especially in moments of disagreement, is a primary barrier to adoption and consistent use. The audit trail allows us to provide persistent transparency and is the mechanism that prevents the AI from creating more work. We need to keep this in mind, particularly when delivering AI-powered tools meant for enterprise use. If the tool delivers a result that misaligns with expectations, you rarely get a second chance. If the user must spend ten minutes investigating why the AI provided that number, they will stop using the AI. Predictability, Reliability, and Understanding Are The Product We are not building magic tricks. A magic trick relies on misdirection and hidden mechanics. We are building colleagues. Think of a good colleague, they keep you in the loop. They let you know what they’re up to, what’s taking their time, and when they hit a snag. That honesty is what helps you trust them. We can apply this to AI. By using the practical patterns we discussed: giving specific updates, showing a dynamic checklist, acknowledging partial wins, and keeping an audit trail, we stop seeing AI as a mysterious black box that just needs a nice coat of paint. Instead, we start treating it like a team member we can rely on and manage, which builds trust and a clear understanding. The main reason for using these interface ideas is to achieve real transparency, going beyond explaining the AI’s complicated inner workings. Here, transparency means showing the user the AI’s process and performance right when they need to see it. This involves plainly communicating the AI’s current status , its known limits , and an easy-to-follow history of its decisions . This level of openness changes the interaction from just accepting what the AI does to actively working with it. It lets users understand why they got a certain result and how they can best step in or guide the system for the best possible outcome. References “ The Essential Guide to A/B Testing ”, Ali E. Noghli “ Usability testing: the complete guide ”, Andrew Tipp “ How to Conduct User Interviews ”, IxDF (yk)Tips on front-end & UX, delivered weekly in your inbox. Just the things you can actually use.
UX Writing FAQs
7 UX Writing: FAQs from Practitioners Anna Kaley Anna Kaley May 8, 2026 2026-05-08 Share Email article Share on LinkedIn Share on Twitter Summary: Get answers to frequently asked questions about UX writing from attendees of NN/G’s Writing Compelling Digital Copy course. As one of the instructors for NN/G’s Writing Compelling Digital Copy course, I address important, timely questions in each training session to teach people what to write, how to structure it, and how to support the experience they’re working on. This article pulls together the questions I get often, grouped by theme. Questions are becoming sharper and more specific as practitioners become more skeptical of AI answers and seek deeper, more nuanced thinking from people with real experience. They may even spark a few new questions of your own about UX writing. In This Article: Plain Language and Readability Tone of Voice Jargon, Acronyms, and Technical Language Structure, Links, and Calls to Action AI, SEO, Stakeholders, and Copy Testing Plain Language and Readability What Reading Level Should I Write for? This is one of the most common questions in training sessions. Aim for a 6th – 8th grade reading level for general audiences. NN/G’s eyetracking studies show that users read only about 20–28% of the text on a page . Writing at a lower grade level helps people process information more quickly and with less effort, regardless of literacy level. You can estimate reading level using tools like the Flesch-Kincaid readability metrics in Microsoft Word, or apps like Hemingway or Grammarly. Learn how to write user-centered content that supports your experience. Writing Compelling Digital Copy draws on eyetracking and usability research to show how people read and what actually works. What You’ll Learn Does Writing at a Lower Reading Level Exclude Educated Audiences? No. This comes up frequently, often framed as: “Our audience is C-suite” or “Our users are doctors.” Even experts are busy, distracted, and scanning on screens. NN/G’s research consistently shows that plain language benefits all literacy levels, including highly educated users. Everyone appreciates saving time and ease of understanding. If Users Read Only 20% of a Page, Does Shortening Text Mean They Read Even Less? Concise writing increases the proportion of content that gets read. When content is well structured, formatted, and written with the right level of concision, the 20% that users do read is more likely to be the right 20%, or the main messages and action items. Does Plain Language Help Neurodiverse Users, Particularly Those with Dyslexia and ADHD? Yes. Plain language, chunked content, descriptive subheadings, and bulleted lists benefit users with dyslexia, ADHD, and other cognitive differences. NN/G’s research on writing for lower-literacy users consistently shows that simplification helps all literacy levels. Tone of Voice What’s the Difference Between Voice and Tone? Voice is your organization’s personality; it should stay consistent. Tone is how that voice adapts to a specific situation and the emotion it evokes in the reader. Your voice might be clear, calm, and direct, but your tone will shift depending on context: more empathetic in an error message, more encouraging in onboarding, more neutral on a policy page. Just like a person, you always sound like yourself, but how you speak at a work meeting differs from how you speak on vacation. Should Tone Vary by Channel and Context? And Should That Be in the Style Guide? Yes. Voice is consistent; tone is contextual. Your brand voice shouldn’t change, but a legal disclaimer calls for a different tone than an onboarding welcome screen. Think about the user’s emotional state: stressed (policy pages), excited (confirmations), or neutral (reference content)? Your content standards or style guide should document both stable voice attributes and tone-shift guidance across contexts and channels. Can AI Tools Detect Tone of Voice? Is That Good Enough? AI tools can approximate tone. They’ll tell you generally if something is friendly or formal, but they can’t predict how that tone lands with your users in their specific context. If the tone is off at the wrong moment (an error message, a billing issue, a sensitive interaction), it can erode trust quickly. The right tone always depends on your specific audience and context. Usability testing and interviewing your real users is still the best way to learn. Jargon, Acronyms, and Technical Language When Is It Okay to Use Jargon? Use jargon only when your audience expects and uses it. Before including a word that could be unclear to some users, make sure you have evidence from user research, survey data, or site-search logs that your audience understands and prefers a technical term. If you haven’t tested or investigated your jargon words, don’t assume people will know what they mean. Words that make sense to you and your internal team (just because you use them all the time!) will not make sense to your users. If your audience is mixed, lead with plain language and include the jargon in parentheses. For terms that appear often, repeat the plain-language version occasionally, as people don’t read everything top to bottom. Should We Spell Acronyms Out Only on First Use? No. Digital readers don’t read top-to-bottom; they scan , enter pages mid-way, jump between sections, and often miss the first-use definition entirely. Just spell out the full phrase the acronym represents, unless it’s so universally recognized that virtually no one in your audience would need the definition. For example, PDF, URL, or FAQ. When in doubt, write it out. Are Branded Terms Jargon? Yes. A hospital link labeled MyCovenantHealth doesn’t tell users whether it leads to a patient portal, bill payment, or appointment scheduling. Patient Records & Insurance communicates purpose immediately. Branded names are meaningful internally but rarely self-explanatory. Always ask: Does this label clearly tell a newcomer what they’ll find if they click on it? Structure, Links, and Calls to Action Should Headings and Subheadings Be Literal or Clever? Literal. Clear, informative headings and s ubheadings are more effective than vague or cutesy labels. Our Approach tells the user nothing. How We Reduce Processing Time by 40% tells them whether to keep reading. Each subheading should summarize the content it introduces. Should We Use Learn More as a Link Label? No, never. Learn More is a lazy link or button label. It tells users nothing about what they’ll learn or where they’ll go after clicking. It’s also a significant accessibility failure: screen readers will announce Learn More, Learn More, Learn More, across a page with no way to distinguish among them. The same issue applies to other vague labels like Shop Now, Discover More , Explore , View Details , or Read More links that describe the action, but not the destination. Use descriptive link text that communicates what users get: See how we reduce processing time is specific and actionable. Should I Include the Word Successfully in Confirmation Messages? No, this word is usually not necessary. You successfully updated your password is redundant. We wouldn’t be confirming the action unless it succeeded. Let the design do the work: a green checkmark communicates success without the word. However, if your brand voice is encouraging and conversational, the word can add warmth. Decide based on your voice guidelines whether adding an extra word is worth it and test it with users. AI, SEO, Stakeholders, and Copy Testing Can I Use AI to Write UX Copy? Yes, but strategically. AI is genuinely useful for first drafts, headline variations, alternative-text generation, microcopy brainstorming, and getting past the blank page. The risks come from overreliance: AI output tends toward the generic, drifts from your brand voice, and has no knowledge of your users, your research, or your context. It also can’t make the judgment calls that define good UX writing, like knowing what to cut, calibrating tone to an emotionally loaded moment, or recognizing when a label will confuse a specific audience. Treat AI as a capable first-draft collaborator . Always edit with your audience in mind, never publish AI copy without review, and keep the strategic thinking. In the Era of AI, What Metrics Should We Use to Evaluate Content Success? Also commonly asked as, Our web traffic is declining as AI tools answer questions directly. If traffic is no longer the right metric, what should I look at instead? Traditionally, traffic was used to assess engagement with content, whether people find, understand, and act on your copy. But as AI tools answer questions directly, site traffic declines and becomes less reliable as a metric for assessing content success. As AI previews intercept more searches before users click, shift measurement focus to engagement and conversion metrics, task completion rates, and return visits. These tell you whether the people who do arrive are getting value. You can use referral traffic from AI tools in your analytics as a rough signal of how often your content appears in AI-generated answers. Just keep in mind it will significantly underrepresent reality, since only a small percentage of users actually click through. In the Age of AI, Does SEO Still Matter and How Does It Relate to AEO, GEO, and All the Other Acronyms? Yes, search-engine optimization (SEO) still matters. Answer-engine optimization (AEO) is about structuring content so AI assistants and voice-search tools can pull a direct answer from it. Generative-engine optimization (GEO) is about making your content authoritative and structured enough that generative AI tools cite it as a source. The underlying principles are the exact same ones that make content work for humans: clear structure, plain language, descriptive headings, direct answers near the top, and genuine authority on your subject. Websites are shifting from being a destination people click on from search-engine-results pages to being a source AI tools quote. The inverted pyramid matters more than ever now, so lead with the answer, then support it. Keyword-stuffed content that was once gamed for search will perform even worse in an AI-mediated world. As we’ve always said, write for people first. Structure it for machines second. How Do I Push Back When My Stakeholders Prioritize SEO over Users? Analytics and heatmaps tell you what users click, not why they struggle. Good UX writing and good SEO are not in conflict with each other. Descriptive headings, clear link text, and plain language support both. Optimize for search to bring users to the page; optimize for users to keep them there. Qualitative research uncovers content problems that no amount of guessing and pontification over a heatmap or analytics report can accurately explain. How Do I Convince Stakeholders that Copy Matters? Show, don’t tell. Eye-tracking videos (which we share in the course ) , usability-test clips, and before-and-after readability scores are more persuasive than process arguments. Frame everything around business outcomes: task-completion rates, time-on-task, and satisfaction all improve with better copy. Are There Affordable Alternatives to Eyetracking? Usability testing , first-click testing , cloze tests , and content-preference tests all surface content problems without the need for expensive hardware. Traditional heatmap tools (which use scroll behavior, dwell time, and cursor positioning rather than hardware and software that’s calibrated to a human’s actual eyes), like Hotjar and Lucky Orange, can provide valuable directional data. Keep in mind, the data provides a very rough approximation and doesn’t tell you about what text people actually paid attention to. If you can’t afford an eyetracking study using the specialized hardware and software from organizations like Tobii , use these as lower-cost approximations of eye-tracking results that practitioners often use for directional guidance. Conclusion Questions and challenges related to UX writing usually stem from organizational constraints: legal requirements, stakeholder preferences, and assumptions about how people read. No matter your skill level with UX writing, using data to support your recommendations, testing with real users, and focusing on how people actually read can lead to better decisions and better experience outcomes. If you’re looking to build more confidence in these areas, NN/G’s Writing Compelling Digital Copy course goes deeper into the research, patterns, and practical writing and editing techniques you can apply right away. These are especially useful if you’re using AI in your content strategy practice or thinking about incorporating it. I look forward to seeing you and answering your questions at an upcoming live online training session. Related Courses Writing Compelling Digital Copy Use UX writing to shape your content to meet your readers’ needs Interaction Content Strategy and Governance Techniques and tools for implementing an actionable content strategy Management Information Architecture Organize and structure information to improve findability and discoverability Interaction Writing for the Web,Content Strategy,Search,ux writing,communication,content,content evaluation,governance,information density,SEO Related Topics Writing for the Web Writing for the Web Content Strategy Search
The Architecture Of Local-First Web Development
What does it really take to build local-first web apps in 2026? A grounded, experience-driven perspective for developers who’ve been doing this long enough to be skeptical of silver bullets. Last October, I was sitting in a hotel room in Lisbon, the night before I was supposed to demo a project management tool my team had spent four months building. The hotel Wi-Fi was doing that thing where it connects but nothing actually loads. And I watched our app, this thing I was genuinely proud of, render a blank screen with a spinner. Then a timeout error. Then nothing. I pulled out my phone, tethered to cellular, and got a shaky connection. The app loaded, but every click was a two-second wait. Create a task? Spinner. Move a task between columns? Spinner. I sat there thinking: we built a front end in React, a back end in Node, a Postgres database, a Redis cache, a GraphQL API with six resolvers just for the task board. All that infrastructure, and the damn thing can’t show me my own data without a round-trip to a server 3,000 miles away. That was the night I started seriously looking at local-first architecture . Not because I read a blog post or saw a tweet. Because I was embarrassed . I want to be upfront about something: I spent the first year or so dismissing local-first as academic. I read the Ink & Switch “Local-First Software” paper when it came out in 2019 and thought, “Cool research, not practical for real apps.” I was wrong. The tooling in 2019 genuinely wasn’t ready. But I was also being lazy, defaulting to the architecture I already knew. The paper laid out seven ideals for software: fast, multi-device, offline, collaboration, longevity, privacy, user ownership . And I remember thinking those sounded like a wish list, not engineering requirements. Seven years later, I’ve shipped three production apps using local-first patterns. I’ve also ripped local-first out of two projects where it was the wrong call. I have opinions. Some of them are probably wrong. But they’re earned. So here’s what I actually think about building local-first web apps in 2026, written for developers who’ve been doing this long enough to be skeptical of silver bullets. Meet Touch Design for Mobile Interfaces , Steven Hoober’s brand-new guide on designing for mobile with proven, universal, human-centric guidelines. 400 pages , jam-packed with in-depth user research and best practices . Jump to table of contents ↬ What “Local-First” Actually Means (And The Confusion That Won’t Die) I need to clear something up because I keep having this conversation at meetups. Local-first is not offline-first. It’s not “add a service worker and call it a day.” It’s not a synonym for PWA. I’ve seen all of these conflated in conference talks, and it drives me a little crazy. Offline-first means your app handles network loss gracefully, but the server is still the source of truth . When the network comes back, the server wins. Cache-first (service workers caching responses) is a performance optimization. You’re serving stale data faster, which is great, but you haven’t changed who owns the data. PWAs are a delivery mechanism: installable, cached, push notifications. None of these is a data architecture. Local-first is a data architecture. Your user’s device holds the primary copy of their data. The app reads and writes to a local database. Renders instantly. Syncs with servers or other devices in the background. The server, when it exists, is a sync peer with some special authority (authentication, backup, access control). But it’s not the gatekeeper. The Ink & Switch paper defined seven ideals, and I think they still hold up. But the one that matters most in practice, the one that changes how you build everything, is this: The client is not a thin view requesting permission to show data. The client is a node in a distributed system with its own database. That distinction sounds subtle. It isn’t. It changes your entire stack. Be Honest Early: When You Should Not Do This I’m putting this near the top because I’ve watched too many developers (including myself, once) get excited about a new architecture and shoehorn it into projects where it doesn’t belong. I wasted about six weeks trying to make a local-first approach work for an internal analytics dashboard at a previous job. My colleague Sarah finally pulled me aside and said, “The data is generated on the server. There’s nothing to replicate to the client. What are you doing?” She was right. Local-first is a bad fit when your data is primarily server-generated. Analytics dashboards, social media feeds, search results: the server produces this data, so the client consuming it via API requests is completely fine. It’s wrong for systems that need strong transactional consistency. Banking, payment processing, and inventory management. If two people try to buy the last item in stock, you need a single authoritative database making that decision with ACID guarantees. Eventual consistency will lose you money, or worse. It’s overkill for simple CRUD apps with no offline or collaboration needs. If you’re building an internal admin panel used by five people in an office with good internet, adding a sync engine is over-engineering. And it’s physically impractical for massive datasets that won’t fit on client devices. But here’s where it shines: note-taking, document editing, collaborative design tools, project management, field apps with unreliable connectivity, basically anything where data privacy is a selling point , as well as anything with real-time collaboration . In other words, it’s great for user-generated data that benefits from instant interaction and should survive the server going down. One more thing I wish someone had told me earlier: you don’t have to go all-in. I’ve had the best results using local-first for specific features within otherwise traditional apps. Offline drafts in a blog editor. Real-time collaborative notes inside a project management tool that’s otherwise standard REST. The “spectrum of local-first” is a real thing, and starting with one feature is how I’d recommend anyone begin. “ Replicas, Not Requests If you’ve used Git, you already understand the mental model. SVN (remember SVN?) was centralized. One server. You check out files, make changes, and commit to the server. Server down? Can’t commit. Can’t even see history. Git gave every developer a full clone. You commit locally, branch locally, and merge locally. Push and pull when you’re ready. The remote repository is important, but it’s not the only copy of the truth. Local-first web development is Git for application data. Every client device holds a replica (full or partial) of the relevant data. Writes happen locally. Sync is push/pull in the background. Conflicts get resolved through defined merge strategies. I remember the first time this clicked for me in practice. I was prototyping a task board, and I wrote a function to add a task. In our old architecture, it would be: POST to API. Wait for the response. If success, update the local state. If failure, show error toast and maybe roll back optimistic update. In the local-first version, it was: write to local SQLite, done. The UI updated instantly because it was reading from the same local database. Sync happened whenever. No loading state, no error handling for the write itself, no optimistic update logic (because there’s nothing to be “optimistic” about; the local write is the state). The implications ripple through everything. You don’t need React Query or SWR for data fetching, because you’re not fetching. You don’t need Redux or Zustand for server-derived state, because the local database is your state. Your routing doesn’t trigger API calls. Authentication works differently because the server isn’t checking permissions on every read. Here’s a visual comparison that might help if you’re the kind of person (like me) who thinks spatially: Traditional request/response architecture vs. local-first architecture. On the left, every user interaction is a round-trip. Click, wait, render. On the right, reads and writes hit the local database directly. The sync server is still there, but it’s doing its work in the background. The user never waits for it. That’s the fundamental shift. But I’m getting ahead of myself. Before we can talk about sync and conflicts, we need to talk about where the data actually lives on the client. Where Data Lives on the Client Forget localStorage . It’s synchronous (blocks the main thread), caps at 5-10 MB, and only stores strings. It’s fine for a theme preference. It’s not a database. IndexedDB is the workhorse that nobody loves. It’s in every browser, it’s asynchronous, it can handle hundreds of megabytes, and its API is absolutely miserable to work with. I’ve used it directly a grand total of once. Now I use it through abstractions or, more often, I don’t use it at all. Because the real story in 2026 is SQLite running in the browser via WebAssembly. I know that sounds like a party trick, but it’s not. SQLite compiled to WASM, persisted to the Origin Private File System (OPFS), gives you a real relational database in the browser. Full SQL queries. Transactions. Indexes. The works. OPFS is the newer API that makes this practical. It gives web apps a sandboxed file system with high-performance synchronous access (in Web Workers), which is exactly what SQLite needs. Before OPFS, you could run SQLite in memory and manually persist to IndexedDB, which worked but was slow and fragile. Here’s roughly what initialization looks like in a real project (I’m using wa-sqlite here, which is the library I’ve had the best luck with): import { SQLiteAPI } from ‘wa-sqlite’; import { OPFSCoopSyncVFS } from ‘wa-sqlite/src/examples/OPFSCoopSyncVFS.js’;
async function initDatabase() { const module = await SQLiteAPI.initialize(); const vfs = new OPFSCoopSyncVFS(‘pm-tool-db’); await vfs.initialize(module);
const db = await module.open_v2(‘workspace.db’);
// HACK: wa-sqlite doesn’t handle concurrent writes well on Safari, // so we serialize through a queue. See vlcn-io/wa-sqlite#247 await module.exec(db, PRAGMA journal_mode=WAL);
await module.exec(db, CREATE TABLE IF NOT EXISTS tasks ( id TEXT PRIMARY KEY, title TEXT NOT NULL, status TEXT DEFAULT 'backlog', assignee_id TEXT, project_id TEXT NOT NULL, position REAL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ) );
return db; } In production, I wrap all database access in a write queue that serializes mutations. I also log every failed write to Sentry with the full SQL statement (scrubbed of PII, obviously) because debugging database issues in a user’s browser is hell without that telemetry. A gotcha I wasted almost two days on: Safari’s OPFS implementation behaves differently from Chrome’s in subtle ways. Specifically, I hit a bug where createSyncAccessHandle() would silently fail in certain iframe contexts on Safari 18. There’s no error, no exception. It just doesn’t work. I ended up falling back to IndexedDB-backed persistence on Safari, which was slower but at least functioned. (I’m told Safari 19 ⁄ 26 fixes this, but I haven’t verified it yet.) Quick comparison of the options I’ve actually used: Storage Good For Watch Out For IndexedDB Broad compatibility, moderate data Terrible DX, no SQL, verbose OPFS + SQLite WASM Relational data, complex queries, serious apps Safari quirks, ~400KB bundle addition PGlite (Postgres in WASM) Full Postgres compatibility on client Newer, larger bundle, still maturing I’ve also tried cr-sqlite , which adds CRDT column support directly to SQLite tables. Clever idea, but I found it too early-stage for production use when I evaluated it in late 2025. The merge semantics were sometimes surprising, and debugging CRDT state inside SQLite was painful. I’d revisit it later this year. The Part That’s Actually Hard Storing data locally is a solved problem. Syncing it reliably across devices and users is where you earn your gray hairs. When multiple replicas can independently read and write, you need a mechanism to reconcile changes. There are basically four approaches, and I’ve used three of them. CRDTs (Conflict-Free Replicated Data Types) are data structures designed so that concurrent edits can always be merged without conflicts, mathematically guaranteed. Yjs is the most popular implementation in JavaScript, and it’s genuinely excellent for real-time collaborative text editing. I used it to build a collaborative document editor at my last company, and the experience was mostly good, though I’ll get into the pain points in the conflict resolution section. Here’s what setting up a shared Yjs document looks like in practice: import * as Y from ‘yjs’; import { WebsocketProvider } from ‘y-websocket’;
const ydoc = new Y.Doc();
const provider = new WebsocketProvider( ‘wss://sync.our-app.dev’, ‘workspace-a1b2c3d4’, ydoc );
const tasks = ydoc.getMap(‘tasks’);
// Add a task const task = new Y.Map(); task.set(‘title’, ‘Review Q3 roadmap draft’); task.set(‘completed’, false); task.set(‘assignee’, ‘maria’); // TODO: type this properly once; yjs exports better TS types // for nested maps. For now, this works fine. tasks.set(‘f47ac10b-58cc-4372-a567-0e02b2c3d479’, task as any);
tasks.observeDeep(() => { // Re-render UI. In practice, I debounce this to ~16ms // because observeDeep fires a LOT during active collaboration renderTaskList(tasks.toJSON()); }); Automerge is the other major CRDT library, backed by Rust and with a document-oriented model. I’ve used it less, but I know teams who swear by it. Loro is newer, Rust-based, and claims better performance. I haven’t shipped anything with Loro yet. Database replication is the other big approach, and honestly, for most apps that don’t need Google Docs-style real-time text editing, I think it’s the better choice. The idea is straightforward: replicate rows between a server database (Postgres) and a client database (SQLite) with a sync engine managing the plumbing. PowerSync does this well. It gives you one-way replication from Postgres to client SQLite with a write-back path for mutations. ElectricSQL is more ambitious, going for full active-active sync between Postgres and SQLite. I’ve used PowerSync in production and ElectricSQL in prototypes. PowerSync felt more stable when I evaluated them both in early 2026, but ElectricSQL’s approach is more powerful if they nail the execution. Triplit takes a different angle entirely: it’s a full-stack database with sync built in, so you don’t think about “client DB” and “server DB” separately. I haven’t tried it beyond a weekend prototype, but the developer experience was surprisingly nice. Event sourcing (syncing a log of mutations rather than the current state) is the approach LiveStore takes. I find it intellectually appealing and occasionally useful, but in practice, I’ve found that reconstructing state from an event log adds complexity that most apps don’t need. My controversial opinion: Event sourcing is over-recommended for application development. It’s great for audit logs and certain domains, but for a task board? Just sync the rows. Not everyone will agree with that. I know event sourcing has passionate advocates, and I’ve been told I’m wrong about this at least twice at conferences. Maybe I just haven’t built the right app for it yet. Conflicts: The Thing Everyone’s Afraid Of I used to think conflict resolution was a terrifying, unsolvable problem. After building three apps that handle it, I’d revise that to: it’s a manageable problem that requires you to think carefully about your specific data model, and most developers overthink it. Conflicts happen when two replicas modify the same data without seeing each other’s changes. User A edits a task title on their phone while offline. User B edits the same title on their laptop. Both come back online. Now what? My first attempt at handling this was embarrassingly naive: // My first try. Don’t do this. function resolveConflict(local: any, remote: any) { // just… take the remote one? sure? return remote; } The problem is obvious: local changes get silently dropped. User A edits a title, syncs, and their edit vanishes. They don’t even know it happened. What actually works for most cases is last-write-wins (LWW) at the field level, not the record level. If User A changes the title and User B changes the due date, you keep both changes because they touched different fields. You only have a real conflict when both modified the same field, and then you pick the later timestamp. interface FieldValue { value: string | number | boolean; // ISO timestamp with enough precision to break most ties updatedAt: string; // Client ID as tiebreaker when timestamps match. // This happens more often than you’d think. clientId: string; }
function pickWinner(a: FieldValue, b: FieldValue): FieldValue { const timeA = new Date(a.updatedAt).getTime(); const timeB = new Date(b.updatedAt).getTime(); if (timeA !== timeB) return timeA > timeB ? a : b; // Deterministic tiebreaker when timestamps match return a.clientId > b.clientId ? a : b; }
// In practice, I apply this per-field across the whole record. function mergeTask(local: Record
async function validateSyncBatch( mutations: SyncMutation[], serverDb: Database ): Promise<{ accepted: SyncMutation[]; violations: SyncViolation[] }> { const accepted: SyncMutation[] = []; const violations: SyncViolation[] = [];
for (const mutation of mutations) { if (mutation.table === ‘calendar_events’) { // Check for double-booking const overlapping = await serverDb.query( SELECT id, title FROM calendar_events WHERE room_id = ? AND id != ? AND start_time < ? AND end_time > ?, [mutation.data.room_id, mutation.data.id, mutation.data.end_time, mutation.data.start_time] );
if (overlapping.length > 0) { violations.push({ type: ‘scheduling_conflict’, recordId: mutation.data.id, description: Conflicts with "${overlapping[0].title}", conflictingRecords: overlapping.map(r => r.id), detectedAt: new Date().toISOString() }); // Still accept the write, but flag it // The alternative is rejecting it, but then the user’s // local state and server state diverge, and that’s worse accepted.push(mutation); continue; } } accepted.push(mutation); }
return { accepted, violations }; } The key decision here — and I went back and forth on this — is that we accept the conflicting write and flag it, rather than rejecting it outright. If you reject it, the user’s local database has a record that the server refuses to acknowledge, and now you’re in a state divergence situation that’s genuinely hard to recover from. I tried the rejection approach first, and it led to ghost records on the client that users couldn’t delete because they didn’t exist on the server. Nightmare. So instead, the server accepts the write, stores the violation, and syncs the violation back to the client. The client shows a non-blocking notification: “Your meeting ‘Q3 Planning’ conflicts with ‘Design Review’ in Room B at 2 PM. Tap to resolve.” The user taps, sees both meetings, and picks one to reschedule or cancel. The resolution is a normal write that syncs back. Is this perfect? No. There’s a window between when the violation is created and when the user resolves it, where both conflicting records exist. For meeting rooms, that’s tolerable. For something like inventory management where two people “buy” the last item, that window is unacceptable, and that’s exactly why I said earlier that local-first is wrong for systems requiring strong transactional consistency. I’m still iterating on this pattern. The violation table grows if users ignore notifications (we expire them after 72 hours, which feels arbitrary). And deciding which invariants to validate on the server requires you to essentially maintain a parallel set of business rules outside your client-side application logic. It’s not elegant. But it works, and it’s the best approach I’ve found for the class of apps I’m building. If you’ve built something cleaner, I genuinely want to hear about it. For CRDTs like Yjs, conflict resolution at the character level (for text) works remarkably well. Two people typing in the same paragraph will see both sets of characters appear in a sensible order. But CRDT merging of structured data (maps, arrays, nested objects) can produce results that surprise you. I once watched a Yjs-backed task list duplicate items after a merge because two users had reordered the same list offline, and the CRDT’s list merge semantics interleaved their orderings. Technically correct. Practically confusing. We ended up adding a post-merge de-duplication step, which felt like a hack but solved the problem. When should you surface conflicts to the user, Git-style? In my experience, almost never for typical app data. Users don’t want to resolve merge conflicts. They want the app to figure it out. The exception is high-stakes content: legal documents, medical records, anything where silently dropping an edit could cause real harm. The Tools Right Now I’m going to give you my honest read on the tools available as of mid-2026, with the caveat that this space is moving fast enough that some of this might be outdated by the time you read it. Yjs is the most mature CRDT library. Production-ready, huge community, integrates with most collaborative editors (TipTap, BlockNote, Lexical). If you need real-time collaborative editing, start here. Automerge is solid, Rust-backed, and takes a more document-oriented approach than Yjs. I’ve seen it used well in apps where the data model fits a document metaphor. Fewer integrations than Yjs, but the core is well-engineered. PowerSync is what I’d recommend for teams that have an existing Postgres back-end and want to add offline support. It’s production-ready, the docs are good, and the mental model (Postgres syncs to client SQLite, client writes go through a defined upload path) is easy to reason about. In our app, initial sync for a workspace with around 5,000 tasks takes about 1.2 seconds on a decent connection and about 3.5 seconds on a throttled 3G simulation. That was acceptable for us. ElectricSQL is going for something more ambitious: true active-active replication between Postgres and SQLite, with “shapes” defining what data syncs to which client. I want this to succeed because the developer experience in prototypes was excellent. But when I evaluated it for production in February 2026, I hit enough rough edges (particularly around shape management and reconnection behavior) that I went with PowerSync instead. I plan to revisit it. Triplit impressed me in a weekend prototype. Full-stack database with sync built in, nice TypeScript API. I haven’t stress-tested it with real production load, and I’d want to before committing. Zero (from Rocicorp, the Replicache people) is interesting because it takes a query-based approach to sync, which is different from the row-replication model. Replicache was sunset in favor of Zero, which tells you something about how fast approaches are evolving in this space. Worth watching, but I wouldn’t build on it yet for a production app. TinyBase is a lightweight reactive store that’s great for smaller apps or prototyping. I used it for a personal side project (a reading tracker) and liked it a lot. Not sure I’d use it for a team-scale product. PGlite (Postgres compiled to WASM) is wild. Same SQL dialect on client and server. Combined with ElectricSQL, you could theoretically run identical queries everywhere. I think this is where things are heading long-term, but PGlite’s bundle size and memory footprint are still concerns for mobile browsers. One thing the Replicache sunset taught me: don’t bet your architecture on a single tool from a small company without a fallback plan. I keep my sync layer abstracted enough that I could swap engines in a few weeks, not months. I know that sounds like premature abstraction, but in a space this young, I think it’s just prudence. Building A Real App: Architecture, Auth, And Migrations I want to walk through how I actually structure a local-first app in practice, because the layer diagrams you see in blog posts rarely match what the code looks like. My current stack for a collaborative project management tool looks like this: UI: React components that never call fetch() for data reads. Query layer: useLiveQuery hooks that subscribe to the local SQLite database and re-render automatically when data changes. Local database: SQLite via wa-sqlite, persisted to OPFS. Mutation layer: Plain INSERT / UPDATE / DELETE statements against local SQLite. Sync: PowerSync managing replication between local SQLite and our Postgres back-end. Server: Postgres, a Node.js auth service, and a small sync validation layer. The component code ends up looking almost absurdly simple compared to what I used to write: import { useLiveQuery } from ‘@powersync/react’; import { db } from ‘../lib/database’;
function TaskBoard({ projectId }: { projectId: string }) { const tasks = useLiveQuery( SELECT * FROM tasks WHERE project_id = ? AND archived = 0 ORDER BY position, [projectId] );
async function addTask(title: string) { await db.execute( INSERT INTO tasks (id, title, project_id, position, created_at) VALUES (?, ?, ?, ?, datetime('now')), [crypto.randomUUID(), title, projectId, tasks.length] ); // That’s it. useLiveQuery picks up the change automatically. // No invalidation, no refetch, no loading state. }
// No isLoading check. Data is local. It’s always there after the first sync. return (
); } Compare that to the React Query + REST equivalent, which would be at least twice the code and include loading states, error states, optimistic update logic with rollback, and cache invalidation. I don’t miss it. Auth In A Local-First World Authentication works roughly the same as traditional apps: JWT tokens, OAuth flows, and session management. The token authenticates the sync connection rather than every individual request. Offline access works because the data is already local. The user was authenticated when the data was originally synced. Authorization is trickier, and I think most local-first articles under-explain this. You cannot sync your entire database to every client and rely on client-side code to hide unauthorized data. Someone will open DevTools, find the local SQLite file, and see everything. The client is not a trust boundary. You enforce authorization at the sync layer. PowerSync has “sync rules” that define which rows go to which clients. ElectricSQL has “shapes.” Either way, the server only sends data that the user is a
Rethinking The Experience Of System Tools
Design always starts with function — function shapes form. But if that function can’t be made completely invisible and people still have to interact with it, it inevitably becomes part of their experience. In this article, Kyrylo Levashov explains why the question has shifted from, “Should your utility software feel better to use?” to “Can your utility software afford not to?“ This article has been kindly supported by our dear friends at MacPaw who not only believe that making great products requires seeing the world in a different light, but also strive to innovate and create incredible software ecosystem for users’ Macs. Thank you! Your grandmother’s vacuum was a trusty but ugly workhorse hidden in a dark closet. Dyson turned that practical tool into an aspirational product, one you love leaving out even when guests come over. Dish soap was just dish soap until Method put it in a glass container, and it became an addition to, not a distraction from, the aesthetics of your kitchen. Physical product brands spent the last two decades transforming mundane, practical items like soap and vacuums into must-have experiences. But utility software — especially maintenance tools, a type of system software designed to analyze, configure, optimize, and maintain a computer — hasn’t made that leap from something you open as a chore to an experience you choose with excitement. And that means those brands are missing an interesting design opportunity: these tools are well overdue for a more intelligent, more human, and less emotionally flat approach. “The Most Underexplored Frontier In UX Is The Maintenance Layer.” Utility software still feels like a chore. Using it has all the excitement of pulling out that dusty old vacuum from the back of the closet. These four common software design assumptions illustrate why the category hasn’t yet transcended its chore status. Assuming the user already resents the task : they’re here because something is wrong, not because they chose to open this tool. Designing accordingly means assuming they want the software to be fast, clinical, invisible, and something to get out of the way, not get into. But a design built for resentment produces tools that deserve it. If you expect your users to want to get out of the product as fast as possible, they’ll feel it in the design. Assuming function is enough and feelings are for consumer apps : emotion in interface design is decoration. The maintenance layer is infrastructure, and nobody decorates infrastructure. But nobody decorated dish soap either, until Method. They didn’t change the product, just the user’s relationship to the tool they use to accomplish a task. Assuming your users are not your fans because nobody cares about maintenance tools : utility tools don’t build communities, and nobody posts about running a disk cleanup. But people care deeply about tools that respect their time and make complex things simple for them to use. The MacPaw team listens to our community and implements many of the features they ask for, because we know users can be fans too, and they should shape how our products work. Assuming that designers shouldn’t waste pixels on personality : you need to hide complexity and show minimal UI. Utility software should look neutral, technical, and forgettable. But when software hides the system, people lose trust in it. Design always starts with function — function shapes form. But if that function can’t be made completely invisible and people still have to interact with it, it inevitably becomes part of their experience. In that case, people expect it not just to work, but to match their environment, influence their mood, and contribute to their overall experience. A good example is a watch. Its core function is simple: show the time. But because a watch occupies physical space in a person’s world, you want more from it than just functionality. It needs to play an aesthetic role and complement the environment. “The Maintenance Layer Is A Behavioral Problem, Not Just A UX One.” The user experience in utility software matters more than the industry tends to admit. In utility software, experience is not something added on top of function. It emerges from how the function is structured, explained, and interacted with. If you think you can design the most functional app on the market without considering how users understand and experience the process, you’re missing an opportunity to build a relationship with that user. Part of that ignored UX element is a behavioral problem: users don’t avoid utility software because using it is hard, but instead because it produces no positive emotional signal at any point. The problem is rarely complex. It’s the absence of meaningful interaction during the process of using the app. Another issue is focusing solely on function. The aesthetic-usability effect shows us clearly that if something looks better, it feels better — ATM screens in a 1995 study were judged easier to use if the screen layout was more attractive. Even something as purely functional as an ATM screen display needs attention to how the function is structured, presented, and perceived. And then there’s the memory problem. People remember the emotional peak and the ending of an experience , not the average. A completed process that ends with a clear “done” is remembered more positively than one that just fades out, even if the end task is completed successfully in both cases. System tools rarely intentionally design the ending of an interaction — they just stop running. “Thoughtful System Design Can Transform Maintenance From A Technical Chore Into A Seamless User Experience.” What does emotional design actually mean, then, in utility UX? Here are three principles the MacPaw team follows to design its products against the category norm. Translating system complexity into human language Maintenance tools deal with storage, task management, and background processes. Good design explains what’s happening, avoids system jargon, and communicates outcomes clearly. Linear’s game-changing move that illustrates this principle was agreeing on straightforward units of work, like projects and teams, that any new user can immediately understand. That helps them spend less time ramping up and more time building. Make the process clear and show progress System tools run complex processes. Design should show progress, impact, and system change to create trust and control. Vercel’s deployment infrastructure is an excellent example here. When you trigger a build, the browser tab favicon changes — a spinner while building, a green checkmark when done, a red X if it fails. It’s ruthlessly functional, not visual or warm, but it’s emotionally intelligent: it exists purely to reduce the low-level anxiety of waiting for a build to finish. Design the moment of completion Maintenance tasks often end quietly. But completion is the emotional payoff. Design should emphasize clarity of results, a sense of resolution, and visible improvement so users remember a positive and distinct ending. Take the new CleanMyMac by MacPaw after its 2024 major update . Unlike the maintenance utility category norm, CleanMyMac uses visual language, including color, depth, motion, icons, and 3D illustrations, to shift the focus from diagnosing problems to showing progress: space cleared, threats removed, time saved. Instead of confronting the user with what’s wrong, the interface closes with a picture of a machine that’s already working better. The task is the same, but the ending tells a different story, giving the user a picture of a machine that’s already working better. “Even if you don’t care about emotional design as a principle, the change is coming anyway.” The market is forcing this issue even for those who don’t find the argument I’ve made here compelling. That’s partly generational — designers and users who grew up with Linear, Figma, and Notion have a completely different baseline for the tools they use. Good software is not a happy accident for them, but a given. That generation is now the primary audience for maintenance software, and so the old “it’s fine, it’s just a utility” excuse doesn’t work philosophically or commercially. Just like Dyson and Method changed how entire product categories approached design, the current state of utility software is shifting for good. And digital fatigue is the current cultural state. The resurgence of vinyl records, film cameras, and dumbphones is not merely nostalgia, but a signal that the emotional relationship between people and their tools is changing. The question has shifted from whether your utility software should feel better to use to whether it can afford not to.
Designing Stable Interfaces For Streaming Content
Streaming UIs are an easy concept on the surface, but are quite complicated in practice. There are many considerations that need to be accounted for, from layout shifts and motion preferences to proper markup and various states, that may not be instantly obvious. What happens if the stream is interrupted? Can users tab through the UI on the keyboard as it shifts? What ARIA attributes might be needed? Those are the sorts of things we will tackle in this article. More interfaces now render while the response is still being generated. The UI begins in one state, then updates as more data comes in. You see this in chat apps, logs, transcription tools, and other real-time systems. The tricky part is that the interface is not in a fixed state ; it keeps changing as new content comes in. It grows where lines become longer and new blocks appear. Something that was just below the screen can suddenly move, and the user’s scroll position becomes harder to manage. Parts of the UI might even be incomplete while the user is already interacting with it. In this article, we’ll take a simple interface and make it handle this properly. We’ll look at how to keep things stable, manage scrolling, and render partial content without breaking the reading experience. What Does A Streaming UI Actually Look Like? I’ve built three demos that stream content in different ways: a chat bubble, a log feed, and a transcription view. They look different on the surface, but they all run into the same three problems. The first is scroll . When content is streaming in, most interfaces keep the viewport pinned to the bottom. That works if you are just watching, but the moment you scroll up to read something, the page snaps back down. You did not ask for that. The interface decided for you, and now you’re fighting it instead of reading. The second is layout shift . Streaming content means containers are constantly growing, and as they do, everything below shifts downward. A button you were about to click is no longer where it was. A line you were reading has moved. The page is not broken; it is just that nothing stays still long enough to interact with comfortably. The third is render frequency . Browsers paint the screen around 60 times per second, but streams can arrive much faster than that. This means the DOM, which is the browser’s internal representation of everything on the page, ends up being updated for frames the user will never actually see. Each update still costs something, and that cost adds up quietly until performance starts to slip. As you go through each demo, pay attention to where things start feeling off. That small moment of friction when the interface starts getting in your way. This is exactly what we are here to fix.
Example 1: Streaming AI Chat Responses This is the most familiar case. You click Stream , and the message starts growing token by token, just like a typical AI chat interface. Open in CodeSandbox . Here’s what I want you to try: Click the Stream button. Try scrolling upwards while the message is streaming. Increase the speed (to something like 10ms). You will notice something subtle but important: the UI keeps trying to pull you back down. Basically, it is making a decision for you about where your attention should be. That’s one example. Let’s look at another. Example 2: Live Processing In A Log Viewer This example looks different on the surface, but the problem is actually very similar to the first example. Rather than a message that gets longer over time, new lines are appended continuously, like a terminal or a log stream. The interesting part here is the tail toggle. It makes the trade-off between interaction and stable interfaces very clear: Open in CodeSandbox . Again, here is what I want you to try: Click the Start button. Allow the logs to stream past the container’s height. Scroll up to the beginning. Stop the stream and disable the “tail” option. Notice that, when tail is enabled, the UI follows the new content. But you’re unable to scroll up and stay in place. Instead, you need to stop the stream or enable “tail” to explore the content. Example 3: Dashboard Displaying Real-Time Metrics In this case, the UI updates in place: Numbers change, Charts shift, Values refresh continuously. Open in CodeSandbox . There is no scroll tension this time, but a different issue shows up. That’s what we’ll get into next. Why The UI Feels Unstable And How To Fix It If you tried the chat demo and scrolled upward while the responses were coming in, you may have spotted the first issue right away: the UI keeps pulling you back down to the latest streamed content as it updates. This takes you out of context and never allows you the time to fully digest the content once it has passed. We see that exact same issue in the second example, the log viewer. Without the tail toggle, the streamed content overrides your scroll position. These aren’t bugs in the traditional sense that they produce code errors; rather, they are accessibility issues that affect all users. That said, they can be fixed and prevented with careful UX considerations as you plan and test your work. Ensure Predictable Scroll Behavior This is the goal: Enable auto-scrolling when detecting that the user is at the bottom of the stream. Stop auto-scrolling when the user has scrolled upwards. Resume auto-scrolling if the user scrolls back to the bottom of the stream. To do that, we need to know whether the user has intentionally moved away from the bottom, which we can assume is true when the scroll position is manually changed. We can track that behavior with a flag. let userScrolled = false;
chatEl.addEventListener(‘scroll’, () => { const gap = chatEl.scrollHeight – chatEl.scrollTop – chatEl.clientHeight;
userScrolled = gap > 60; }); That 60px threshold matters. Without it, tiny layout changes (like a new line) would briefly create a gap and break auto-scroll, even if the user didn’t actually scroll. Now let’s make sure that we enable auto-scrolling only when the user’s scroll position is equal to the stream’s scroll height, i.e., the user is at the bottom of the stream: function autoScroll() { if (!userScrolled) { chatEl.scrollTop = chatEl.scrollHeight; } } One small thing that’s easy to miss: we need to reset userScrolled once a new stream begins. Otherwise, one scroll from a previous message can silently disable auto-scroll for the next one. Solidify Layout Stability We saw this in the first example as well. As new content streams in, the layout jumps, or shifts, taking you out of your current context. To be specific about what’s shifting: it’s not the page layout in a broad sense, it’s the content directly below the chat bubble. There’s also a subtler artifact worth calling out before we look at the code: cursor flicker. Because we’re wiping innerHTML and recreating every element on every tick, the cursor is being destroyed and re-added constantly, up to 80 times per second at fast speeds. At normal speed, it’s easy to miss, but slow the slider down to around 30ms, and you’ll see a faint but persistent flicker at the end of the text. Once we fix the rebuild pattern, the flicker disappears entirely. That rebuild pattern is right here; this is what runs on every single incoming character: bubble.innerHTML = ”;
fullText.split(‘\n’).forEach(line => { const p = document.createElement(‘p’); p.textContent = line || ‘\u00A0’; bubble.appendChild(p); });
bubble.appendChild(cursorEl); This works, but it’s expensive. Every update wipes the DOM and rebuilds it, forcing layout recalculation each time. Now we write directly into a live node: let currentP = null;
function initBubble(bubble, cursor) { currentP = document.createElement(‘p’); currentP.appendChild(document.createTextNode(”)); bubble.insertBefore(currentP, cursor); } What we can do next is to create one paragraph with an empty text node and insert it before the cursor. That gives us a live node we can write into directly. Then, for each character that arrives: function appendChar(char, bubble, cursor) { if (char === ‘\n’) { currentP = document.createElement(‘p’); currentP.appendChild(document.createTextNode(”)); bubble.insertBefore(currentP, cursor); } else { currentP.firstChild.textContent += char; } } For a regular character, we extend the text node by one character. The browser doesn’t need to recalculate the layout for that; the text grew, but nothing moved. For a newline, we create a fresh paragraph and move currentP forward. Layout recalculates once for that new paragraph, and that’s it. Render Frequency This one is most visible in the first example, the chat UI. Even with scrolling and a layout fixed, we’re still writing to the DOM on every single incoming character. When the stream is moving fast, you end up hammering the DOM with updates that don’t actually matter. The fix is straightforward: hold the incoming text in a buffer instead of writing it out immediately. Once you’ve collected enough, write it all to the DOM in one go; that’s what a flush is. To pull this off, we keep a simple buffer and make sure we only schedule a single update at a time. When it fires, requestAnimationFrame takes everything that has built up and writes it to the DOM in one shot. let pending = ”; let rafQueued = false; When a new character streams in, we then add it to the buffer. If no flush is scheduled yet, we queue one: function onChar(char) { pending += char;
if (!rafQueued) { rafQueued = true; requestAnimationFrame(flush); } } The rafQueued flag is important. Without it, every character would schedule its own frame, and you’d end up with dozens of unnecessary flushes. When the flush fires, it drains the entire buffer in one pass: function flush() { for (const char of pending) { appendChar(char); } pending = ”; rafQueued = false; autoScroll(); } All the characters that arrive after the last frame are then rendered together, right before the browser paints them. Then we clear the buffer, reset the flag, and run auto-scroll once. let userScrolled = false;
chatEl.addEventListener(‘scroll’, () => { const gap = chatEl.scrollHeight – chatEl.scrollTop – chatEl.clientHeight;
userScrolled = gap > 60; });
function autoScroll() { if (!userScrolled) { chatEl.scrollTop = chatEl.scrollHeight; } } If the gap is small, we keep auto-scrolling. If it grows, we assume the user scrolled up, and we stop. That small threshold helps avoid jitter when new lines slightly change the height. Also, remember to reset userScrolled when a new stream starts. Once scrolling is under control, another issue becomes obvious. As the message grows, it keeps shifting: It starts as one line, It expands, then It pushes everything below it. Nothing is technically broken, but it doesn’t feel stable. A common approach is to rebuild the whole message on every update: bubble.innerHTML = ”;
fullText.split(‘\n’).forEach(line => { const p = document.createElement(‘p’); p.textContent = line || ‘\u00A0’; bubble.appendChild(p); });
bubble.appendChild(cursorEl); This works, but it is doing too much work. Every update destroys and rebuilds the DOM, forcing layout recalculation each time. That’s why everything keeps shifting. The idea is to write into the current paragraph and only create a new one when we actually hit a line break. let currentP = null;
function initBubble(bubble, cursor) { currentP = document.createElement(‘p’); currentP.appendChild(document.createTextNode(”)); bubble.insertBefore(currentP, cursor); } And then update it character by character: function appendChar(char, bubble, cursor) { if (char === ‘\n’) { currentP = document.createElement(‘p’); currentP.appendChild(document.createTextNode(”)); bubble.insertBefore(currentP, cursor); } else { currentP.firstChild.textContent += char; } } Now we’re no longer rebuilding everything. Most updates just extend a text node, which is cheap and doesn’t trigger large layout shifts. It also fixes the small cursor flicker you might have noticed earlier, since we’re no longer removing and re-adding it. At this point, the UI already feels better, but there is still something subtle going on. We are still updating the DOM on every character. At higher speeds, that becomes a lot of small updates, many of which you never actually see. Instead of rendering immediately, we can buffer the incoming characters and apply them once per frame. let pending = ”; let rafQueued = false;
function onChar(char) { pending += char;
if (!rafQueued) { rafQueued = true; requestAnimationFrame(flush); } } At this point, we’re not touching the DOM yet, but only collecting characters as they arrive. Then, right before the next frame is painted, we flush everything at once: function flush() { for (const char of pending) { appendChar(char); }
pending = ”; rafQueued = false;
autoScroll(); } These separate two things that were previously tied together: How fast data arrives, and When the UI updates. The result looks the same, but the browser does less work, resulting in the UI feeling smoother, especially when the stream is set to a faster speed. Open in CodeSandbox . None of these changes is a big effort on its own. But once they are in place, the interface stops reacting blindly to every update. It becomes easier to read, easier to control, and a lot less distracting, even though the content is still coming in continuously. There are even more considerations to take into account for ensuring a stable, predictable, and good user experience. For example, what happens if the stream is canceled mid-flow? And what can we do to ensure that user preferences are respected for things like reduced motion, keyboard navigation, and screen reader accessibility? Let’s get into those next. Handling Interrupted Streams Most streaming interfaces include a way to stop or cancel the stream. We saw that in the demos. But stopping often leaves the UI in an awkward state. The cursor might keep blinking, buttons don’t update, and the message just freezes mid-stream with no clear indication that it didn’t finish. The problem is that the stop is usually wired to do one thing: cancel the timer. That’s not enough. You also need to (1) clear the pending buffer, (2) remove the cursor, (3) mark the response as incomplete, and (4) reset the buttons. Here’s how we accomplish those. 1. Stop The Stream Cleanly Here’s what stopStream needs to do, in order: Cancel the timer and flip the isStreaming flag so no more ticks run. Clear the requestAnimationFrame (RAF) buffer so nothing still queued gets written on the next frame. function stopStream() { clearTimeout(streamTimer); isStreaming = false; pending = ”; rafQueued = false; } Clearing the pending property matters because there might be characters buffered from the last stream instance that haven’t been flushed yet. If you don’t clear it, the next requestAnimationFrame fires, drains the buffer, and writes those characters to the DOM after the stream has officially stopped. Now we move on to removing the cursor by calling markStopped on the bubble: if (cursorEl && cursorEl.parentNode) cursorEl.remove(); markStopped(aiBubble);
stopBtn.style.display = ‘none’; retryBtn.style.display = ”; playBtn.style.display = ”; setStatus(‘Stopped’, ‘stopped’); chat.removeEventListener(‘scroll’, onScroll); } The cursorEl.parentNode check is there because stopStream is also called internally when a new message fires mid-stream, at which point the cursor might already be gone. Calling remove() on a detached node throws, so we check first. markStopped appends a small label to the bottom of the bubble so the user knows the response didn’t finish: function markStopped(bubble) { if (!bubble) return; bubble.classList.add(‘stopped’);
const label = document.createElement(‘span’); label.className = ‘stopped-label’; label.textContent = ‘response stopped’; bubble.appendChild(label); } The null check on bubble handles the edge case where stop fires before the AI message element has been initialized, which can happen if the user clicks stop during the 300ms delay before the bubble appears. Provide A Retry Option If the stream simply stops — perhaps due to a network issue or some other unexpected error — we ought to provide the user with a path to re-attempt the stream. What that basically means is preventing the UI from doing the expensive work needed to scroll back up to the top, re-read the prompt, and retype it. With a retry option, the user only needs to click a button, and the stream restarts from the current position. To make that work, we need to hold onto the question when the stream starts: let lastQuestion = ”;
function startStream(question, answer) { lastQuestion = question; // rest of setup… } Then, when the retry attempt runs, we reset everything and start fresh: function retryStream() { if (currentMsgEl && currentMsgEl.parentNode) { currentMsgEl.remove(); }
charIndex = 0; userScrolled = false; pending = ”; rafQueued = false; isStreaming = true;
retryBtn.style.display = ‘none’; stopBtn.style.display = ”; setStatus(‘Streaming…’, ‘streaming’);
chat.addEventListener(‘scroll’, onScroll, { passive: true });
setTimeout(() => { initAIMsg(); tick(lastAnswer); }, 200); } The reset is critical. Every piece of state needs to go back to its initial value, just like a brand new stream. Note: We remove the entire message row ( currentMsgEl ), not just the bubble. If only the bubble is removed, the layout wrapper and avatar remain persistent and break the structure. Send A New Message Mid-Stream There’s one more edge case that’s easy to miss. If the user sends a new message while a stream is still running, you end up with two loops writing to the DOM at the same time. The result is messy, and characters from different responses get mixed together. Here’s what to do: stop the current stream before starting a new one. function startStream(question, answer) { if (isStreaming) { clearTimeout(streamTimer); isStreaming = false; pending = ”; rafQueued = false; if (cursorEl && cursorEl.parentNode) cursorEl.remove(); chat.removeEventListener(‘scroll’, onScroll); }
// now reset and start fresh charIndex = 0; userScrolled = false; isStreaming = true; lastQuestion = question; // … } Here, we inline the cleanup rather than calling stopStream directly because stopStream also calls markStopped and resets the buttons. The next demo has all three behaviors wired up. You can start a stream, hit “Stop” mid-stream, and the cursor disappears, the “response stopped” label appears, and a “Retry” buttons displayed. Open in CodeSandbox . Accessibility Streaming interfaces are often built and tested with a mouse, so they may feel just fine in a browser, but break down in other situations that may not have been considered, like whether a screen reader announces new content at all. Or navigating with a keyboard might get stuck or lose focus as things update. And, of course, moving text can be uncomfortable — or even disabling — for those with motion sensitivities . The good part is that you do not need to rebuild everything to accommodate these things; they can be fixed with solutions that sit on top of what is already there. Accommodating Assistive Technology With Live Regions Screen readers don’t automatically announce content that shows up on its own. They usually read things when the user moves to them. So, in a streaming UI, where text builds up over time, nothing gets announced. The content is there, but the user doesn’t hear anything. The fix is aria-live . It tells the browser to watch a container and announce updates as they happen, without the user needing to move focus.
role=”log” tells assistive tech this is a stream of updates, like a running transcript. Some tools handle this automatically, but it’s safer to be explicit so behavior stays consistent. aria-atomic=”false” makes sure only the new content is announced. Without it, some screen readers try to read the whole message again on every update, which quickly becomes unusable. aria-live=”polite” queues updates instead of interrupting. Use assertive only for things that really need immediate attention, like errors. Handling Incomplete States Earlier, we inserted a “Response Stopped” label to the message when the stream stops mid-stream. Visually, that’s enough. But for a screen reader, that change needs to be announced. Since the message is inside a live region with aria-live=”polite” , the label will be automatically announced as new content when it’s added to the DOM. The live region already handles the announcement, so no additional ARIA is needed on the label itself. The Retry button that appears next also needs context. If a screen reader simply says “Retry, button,” it’s not clear what action that refers to. You can fix that by adding an aria-label that includes the original question: retryBtn.setAttribute( ‘aria-label’, Retry: ${lastQuestion.slice(0, 60)} ); What you can do here is to set this label when the button appears, not on page load: retryBtn.style.display = ‘inline-block’; retryBtn.setAttribute( ‘aria-label’, Retry: ${lastQuestion.slice(0, 60)} ); We also call retryBtn.focus() after stopping. That way, keyboard users don’t have to Tab around with the keyboard to find the next action. Testing with assistive technology: Don’t rely on assumptions about how screen readers announce this. Test with actual tools like NVDA (Windows), JAWS (Windows), or VoiceOver (Mac/iOS). Browser DevTools can show you what’s exposed in the accessibility tree, but they can’t tell you how the content sounds . A real screen reader will reveal whether the announcement is happening at the right time and in the right way. Account For Keyboard Navigation The controls need to work with the keyboard while the UI is live, so the Stop button has to be reachable. For someone not using a mouse, Tab + Enter is the only way to cancel a running stream. Using display: none is fine for hiding buttons; it removes them from the tab order. The problem is using things like opacity: 0 or visibility: hidden . Those hide elements visually, but they can still receive focus, so users end up tabbing onto something they can’t see. Use :focus-visible so the focus ring shows up for keyboard navigation, but not for mouse clicks: btn:focus-visible { outline: 2px solid #1d9e75; outline-offset: 2px; } The cursor inside the message should have aria-hidden=”true” . It’s just visual. Without that, some screen readers try to read it as text, which gets distracting. Motion Sensitivity The typewriter effect we see in practically every AI interface produces constant motion. As we’ve already discussed, certain amounts of motion can be disabling. Thankfully, browsers expose prefers-reduced-motion , which detects a user’s motion preferences at the operating system level. For streaming, the best approach is simple: skip the animation and render the full response at once. The content stays the same, only without the motion. const reducedMotion = window.matchMedia( ‘(prefers-reduced-motion: reduce)’ ).matches; if (reducedMotion) { initAIMsg(); for (const char of text) appendChar(char); if (cursorEl && cursorEl.parentNode) cursorEl.remove(); done(); return; } tick(text); // normal animation In CSS, the cursor blink also needs to stop. Despite being a minor detail, a blinking cursor element counts as flashing content . @media (prefers-reduced-motion: reduce) { .cursor { animation: none; opacity: 1; } } There we go! The demo below puts everything from this article together, so you can see how these patterns work in practice. It also includes a reduced motion toggle, so you can test the instant render version easily. Open in CodeSandbox . Conclusion Streaming itself is mostly solved. Getting data from the server to the client is not the hard part anymore. What breaks is the UI on top of it. When content updates continuously, small things start to matter, like scroll behavior, layout stability, render timing, and how the interface responds to user actions. If those aren’t handled well, the UI feels unstable and hard to use. The patterns in this article fix that by: Keeping scroll position under the user’s control, Updating only what has changed, Batching renders per frame, Handling stop and retry actions, and Making the interface accessible. You don’t need all of these every time. But when streaming is involved, these are the places things usually go wrong. Further Reading Using Server-Sent Events How to open a connection, handle events, and reconnect when needed. This is the transport layer, everything here builds on. Streams API Streaming data directly from fetch . Useful when you need more control than SSE. Chrome DevTools Performance panel Helps you see layout recalculations and paint costs, so you can verify performance improvements. “ How Large DOM Sizes Affect Interactivity, And What You Can Do About It ”, Jeremy Wagner Why large DOM trees slow things down, and how to keep them under control in long streaming sessions. (yk)Tips on front-end & UX, delivered weekly in your inbox. Just the things you can actually use.
A Fresh View In May (2026 Wallpapers Edition)
Let’s welcome May with a new collection of desktop wallpapers! Following our monthly tradition, the wallpapers were created by the community for the community and can be downloaded for free. Enjoy! May has a way of sneaking in with longer days, softer light, and that first real hint of summer in the air. It’s the season of fresh ideas and just enough energy to start something new , or finally pick up something you’ve been putting off. And sometimes, all it takes to spark that little bit of inspiration is a fresh view… even if it’s just on your desktop. That’s where our monthly wallpapers series comes in. For the past 15 years, artists and designers from around the world have been contributing their designs to celebrate each new month. This May is no exception. Created with care and a unique personal touch , every wallpaper in this collection comes in a variety of screen resolutions and can be downloaded for free. A huge thank-you to everyone who got creative — this post wouldn’t be possible without your wonderful support! If you too would like to get featured in one of our upcoming wallpapers posts, please don’t hesitate to join in . We can’t wait to see what you’ll come up with! Happy May! You can click on every image to see a larger preview . We respect and carefully consider the ideas and motivation behind each and every artist’s work. This is why we give all artists the full freedom to explore their creativity and express emotions and experience through their works. This is also why the themes of the wallpapers weren’t anyhow influenced by us but rather designed from scratch by the artists themselves. Happily Invisible Online Designed by Ricardo Gimenes from Spain. preview with calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Where Every Sip Tells A Secret “A quiet ritual, a shared moment, a pause in the rush — tea invites you to slow down and discover warmth in the smallest details. Let each cup unfold its own little story.” — Designed by PopArt Studio from Novi Sad, Serbia. preview with calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Just A Style Thing Designed by Ricardo Gimenes from Spain. preview with calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Next Bloom “A small bee with a big garden plan checks each flower on her list and looks for the next bloom to visit.” — Designed by Ginger IT Solutions from Serbia. preview with calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 No Play Jack “Summer is getting closer, but we’re reminded of a more wintry and eerie landscape, like that of ‘The Shining.’ A truly great film, proving that you don’t need much, but it needs to be used well to create suspense and terror.” — Designed by Veronica Valenzuela from Spain. preview with calendar: 640×480 , 800×480 , 1024×768 , 1280×720 , 1280×800 , 1440×900 , 1600×1200 , 1920×1080 , 1920×1440 , 2560×1440 without calendar: 640×480 , 800×480 , 1024×768 , 1280×720 , 1280×800 , 1440×900 , 1600×1200 , 1920×1080 , 1920×1440 , 2560×1440 Buddha Purnima “Buddha Purnima, falling on May 1st, is the most sacred Buddhist festival commemorating the birth, enlightenment, and passing of Gautama Buddha. It is observed on the full moon day of the Vaisakha month, symbolizing spiritual liberation and the triumph of peace. The day serves as a global reminder of his core teachings: non-violence, compassion, and the path to ending suffering.” — Designed by V D Photography from Surat, Gujarat, India. preview with calendar: 1280×720 , 1920×1080 , 2560×1440 , 3840×2160 without calendar: 1280×720 , 1920×1080 , 2560×1440 , 3840×2160 Meet “TypeScript in 50 Lessons” , our shiny new guide to TypeScript. With detailed code walkthroughs , hands-on examples and common gotchas. For developers who know enough JavaScript to be dangerous. Jump to table of contents ↬ Hello May “The longing for warmth, flowers in bloom, and new beginnings is finally over as we welcome the month of May. From celebrating nature on the days of turtles and birds to marking the days of our favorite wine and macarons, the historical celebrations of the International Workers’ Day, Cinco de Mayo, and Victory Day, to the unforgettable ‘May the Fourth be with you’, May is a time of celebration — so make every May day count!” — Designed by PopArt Studio from Serbia. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1440×900 , 1440×1050 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Add Color To Your Life! “This month is dedicated to flowers, to join us and brighten our days giving a little more color to our daily life.” — Designed by Verónica Valenzuela Jimenez from Spain. preview without calendar: 800×480 , 1024×768 , 1152×864 , 1280×800 , 1280×960 , 1440×900 , 1680×1200 , 1920×1080 , 2560×1440 Ladies And Gentlemen Designed by Ricardo Gimenes from Spain. preview without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Poppies Paradise Designed by Nathalie Ouederni from France. preview without calendar: 320×480 , 1024×768 , 1280×1024 , 1440×900 , 1680×1200 , 1920×1200 , 2560×1440 Understand Yourself “Sunsets in May are the best way to understand who you are and where you are heading. Let’s think more!” — Designed by Igor Izhik from Canada. preview without calendar: 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Navigating The Amazon “We are in May, the spring month par excellence, and we celebrate it in the Amazon jungle.” — Designed by Veronica Valenzuela Jimenez from Spain. preview without calendar: 640×480 , 800×480 , 1024×768 , 1280×720 , 1280×800 , 1440×900 , 1600×1200 , 1920×1080 , 1920×1440 , 2560×1440 ARRR2-D2 Designed by Ricardo Gimenes from Spain. preview without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Lake Deck “I wanted to make a big painterly vista with some mountains and a deck and such.” — Designed by Mike Healy from Australia. preview without calendar: 1280×960 , 1440×900 , 1680×1050 , 1920×1080 , 2560×1440 , 2560×1600 , 2880×1800 Today, Yesterday, Or Tomorrow Designed by Alma Hoffmann from the United States. preview without calendar: 1024×768 , 1024×1024 , 1280×800 , 1280×1024 , 1366×768 , 1440×900 , 1680×1050 , 1920×1080 , 1920×1200 , 2560×1440 The Monolith Designed by Ricardo Gimenes from Spain. preview without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Tentacles Designed by Julie Lapointe from Canada. preview without calendar: 320×480 , 1024×768 , 1280×800 , 1280×1024 , 1440×900 , 1680×1050 , 1920×1200 Geo Designed by Amanda Focht from the United States. preview without calendar: 320×480 , 640×480 , 800×480 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1680×1200 , 1920×1080 , 1920×1440 , 2560×1440 Make A Wish Designed by Julia Versinina from Chicago, USA. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Bat Traffic Designed by Ricardo Gimenes from Spain. preview without calendar: 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 , 3840×2160 Blooming May “In spring, especially in May, we all want bright colors and lightness, which were not there in winter.” — Designed by MasterBundles from Ukraine. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Enjoy May! “Springtime, especially May, is my favorite time of the year. And I like popsicles — so it’s obvious isn’t it?” — Designed by Steffen Weiß from Germany. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Stone Dahlias Designed by Rachel Hines from the United States. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×1024 , 1366×768 , 1400×900 , 1400×1050 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Spring Gracefulness “We don’t usually count the breaths we take, but observing nature in May, we can’t count our breaths being taken away.” — Designed by Ana Masnikosa from Belgrade, Serbia. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Sweet Lily Of The Valley “The ‘lily of the valley’ came earlier this year. In France, we celebrate the month of May with this plant.” — Designed by Philippe Brouard from France. preview without calendar: 800×480 , 1024×768 , 1024×1024 , 1280×720 , 1280×1024 , 1440×900 , 1920×1080 , 1920×1440 , 2560×1440 April Showers Bring Magnolia Flowers “April and May are usually when everything starts to bloom, especially the magnolia trees. I live in an area where there are many and when the wind blows, the petals make it look like snow is falling.” — Designed by Sarah Masucci from the United States. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Always Seek Knowledge “‘As knowledge increases, wonder deepens.’ (Charles Morgan) So I tried to create an illustration based on this.” — Designed by Bisakha Datta from India. preview without calendar: 320×480 , 640×480 , 800×480 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×900 , 1400×1050 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 May Your May Be Magnificent “May should be as bright and colorful as this calendar! That’s why our designers chose these juicy colors.” — Designed by MasterBundles from Ukraine. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Celestial Longitude Of 45° “Lixia is the 7th solar term according to the traditional East Asian calendars, which divide a year into 24 solar terms. It signifies the beginning of summer in East Asian cultures. Usually begins around May 5 and ends around May 21.” — Designed by Hong, Zi-Cing from Taiwan. preview without calendar: 1024×768 , 1080×1920 , 1280×720 , 1280×800 , 1280×960 , 1366×768 , 1400×1050 , 1680×1050 , 1920×1080 , 1920×1200 , 2560×1440 Power Designed by Elise Vanoorbeek from Belgium. preview without calendar: 1024×768 , 1280×800 , 1280×1024 , 1440×900 , 1680×1050 , 1920×1200 , 2560×1440 Rainy Days “Winter is nearly here in my part of the world and I think rainy days should be spent at home with a good book!” — Designed by Tazi Design from Australia. preview without calendar: 320×480 , 640×480 , 800×600 , 1024×768 , 1152×864 , 1280×720 , 1280×960 , 1600×1200 , 1920×1080 , 1920×1440 , 2560×1440 Birds Of May “Inspired by a little-known ‘holiday’ on May 4th known as ‘Bird Day’. It is the first holiday in the United States celebrating birds. Hurray for birds!” — Designed by Clarity Creative Group from Orlando, FL. preview without calendar: 320×480 , 640×480 , 640×960 , 640×1136 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Magical Sunset “I designed Magical Sunset as a friendly reminder to take a moment and enjoy life around you. Each sunset and sunrise brings a new day for greatness and a little magic.” — Designed by Carolyn Warcup from the United States. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1400×1050 , 1440×900 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 All Is Possible In May “Edwin Way Teale once said that ‘[t]he world’s favorite season is the spring. All things seem possible in May.’ Now that the entire nature is clothed with grass and branches full of blossoms that will grow into fruit, we cannot help going out and enjoying every scent, every sound, every joyful movement of nature’s creatures. Make this May the best so far!” — Designed by PopArt Studio from Serbia. preview without calendar: 320×480 , 640×480 , 800×480 , 800×600 , 1024×768 , 1024×1024 , 1152×864 , 1280×720 , 1280×800 , 1280×960 , 1280×1024 , 1366×768 , 1440×900 , 1440×1050 , 1600×1200 , 1680×1050 , 1680×1200 , 1920×1080 , 1920×1200 , 1920×1440 , 2560×1440 Get Featured Next Month Feeling inspired? We’ll publish the June wallpapers on May 31, so if you’d like to be part of the collection, please don’t hesitate to submit your design . We are already looking forward to it!Tips on front-end & UX, delivered weekly in your inbox. Just the things you can actually use.