With the proliferation of chatbots, there is a lot of attention towards conversational UIs. But every UI is a conversation - between your application and the user. As engineers, we tend to focus more on what our application needs and what we need from the user. That works, but if you want to make a good UI, you have to enter the user’s mind and focus on what they are trying to accomplish and create an experience that works for them. Like any good conversation, make it intuitive, thoughtful, and delightful.
Ten years ago, that was hard to do technically, but today, in the asynchronous world we live in, there is no excuse to be sloppy - there are many frameworks available to create thoughtful user interfaces. All we need is common sense and thoughtfulness.
Let’s take a look at this credit card form:
This works from an application standpoint, but there’s a number of things that are thoughtless about it:
Why should accidentally double-clicking create a problem for the user? Although users double clicking on a submit button is rare, we can ignore the second click by removing the event handler attached to the submit button right after the first click event, instead of troubling the user. After all, a double click event is nothing but two click events that happened rapidly. Handling both click and double click events on a single DOM element is not natural. What we need here is to act only for the very first click event.
Why ask the user to enter card type? We can read the credit card number and detect the card type and use that. This is unnecessary extra work for the user.
Why a pick list for the expiration date? It’s a lot easier for the user to just enter four numbers instead of reaching for the mouse and pulling down the menus and selecting the month and year of their expiration date.
This is a classic example of “application thinking” instead of “user thinking.” When designing this UI, if we enter the user’s mind, we’ll notice that they are here to simply enter their credit card and complete the checkout process. They either know their credit card information or have the card in front of them. And there are three things that matter to them:
I want to quickly enter my card information with as little work as possible and move on.
I want to know if my card is supported or not (or which card to use).
I want assurance that entering my credit card information is safe here.
If we start there, this form would look a lot different.
Let’s take another example - a smart selection combo box with an instant search that calls a backend API to get a list of relevant options based on what the user is typing.
A simple way to do this, and one that most novice engineers do, is to use the onKeyDown event on the text box to trigger the API call and populate the results in a div and show them. While this works nicely in general, there are many scenarios where this can trigger a lot of API calls in a short time. Imagine what happens if the user types too fast and it triggers a query for each character typed - what would the experience look like? If not handled carefully, that can stress out the backend and freeze the page.
Again, if we enter the user’s mind and think about all the ways this can be used and abused, a better way to do it then is to only call the backend API after, say, half a second or a second of inactivity.
One more common example - error messages.
When something goes wrong in the application, we tend to come up with messages “stating the fact.” For example, say you’re building a social app and when the user clicks on “Friends” that list is empty. It’s natural to say “No Friends Yet” because that’s the fact and you’re stating it. But if you enter the user’s mind, “No Friends Yet” is not helpful. A better message is to say you don’t have any friends yet and here’s how you can add friends. An even better thing to do is to show why adding friends makes this a much better application overall.
So, when developing user interfaces, it is important to enter the user’s mind and start there:
What are they trying to accomplish and what is their state of mind? In the first example, they probably already did a bunch of things before getting to the credit card page and are therefore anxious to enter the card information and move on.
What do they need to know in advance so the process goes smoothly? For example, if you’re going to ask them for information they might not have handy, it helps to tell them that beforehand so they can get it before they begin the process.
How can we make their life easy? Detect and infer anything that can be inferred rather than making them enter the information explicitly, collect only the minimum information needed, etc.
How can we assure them that it’s safe to do what we’re asking them to do? Telling them that connecting their Facebook account does not automatically allow us to post to their timeline, that their sensitive information will be transmitted safely and not stored anywhere, etc.
How can we automatically prevent intentional or accidental abuse? Thinking about all possible cases including fat fingers or fast typing or random strings or intentional abuse and handling those cases elegantly.
Frameworks like RxJS simplify the complexity in developing thoughtful interfaces. RxJS opens up the paradigm of thinking of asynchronous event streams as data sequences happening over time and you can subscribe to these event streams using Observables. The Observable notifies the subscribed observers when an event occurs. As Observables are composable, that gives the power to conditionally compose asynchronous operations using RxJS Operators. Let’s see how these operators can solve the two problems above.
In the first case, we want to prevent the user from clicking on the submit button more than once. In the RxJS world, the user clicking on the submit button is the click event stream. The RxJS First operator gives us only the first item emitted by an Observable and ignores the rest, and that solves our problem. Here is the link to the RxJS visualizer.
const button = document.createElement('button'); button.innerHTML="Click me" output.append(button) let clickedEmittedCount = 0; Rx.Observable .fromEvent(button, 'click') .first() .map(e => clickedEmittedCount++);
For the second problem, if we think of onKeyDown as the event stream that we subscribed to, then the RxJS operator Debounce will help us react to fire an API only when there is no user action for the given period of time. Here is the link to the RxJS visualizer for this code snippet.
const input = document.createElement('input'); input.setAttribute('placeholder', 'Type something'); output.prepend(input); input.focus(); Rx.Observable .fromEvent(input, 'keydown') .debounceTime(1000) .map(e => input.value);
Every user interface is a conversation between your application and your user. Make it thoughtful and delightful.
When designing user interfaces, enter the user’s mind and start there - what are they trying to accomplish? And how can we make it easy, safe, and delightful?
Be as thoughtful as you can be about making users work - think hard about other ways you can get the information you’re asking the user to enter or the work you’re making them do.
Think hard about accidental and intentional abuse and automatically handle it elegantly.
Leverage modern frameworks like RxJS that make asynchronous event handling easier in order to make your UI intuitive and easy.
Thanks to Charles Cho for helping with this post.