I wrote this post for the CodeSandbox team, we're an online code editor that has a big focus on building a user-friendly application. This document serves as a guideline for everyone working on CodeSandbox, but I think it applies to almost any product. This document is never really finished, I'll try to keep it up to date with amendments/additions that may come! Let me know what you think should be added!
With CodeSandbox we value a fast response time and good product experience. I decided to compile a list of best practices on UI/UX and development (coding) to ensure that we, as a team, can keep these principles. This list is not complete: it only highlights the practices which I find most important. Also, keep in mind that these are guidelines, and might not apply to all scenarios. With this, our product (the app) keeps its feel of good UX/UI and we'll be able to maintain our speed and quality when building CodeSandbox.
With CodeSandbox we want to be an easy to use application with the productivity of a power tool. This is quite a challenge, which is why it's good to write down our best practices on UX.
Some other applications that serve as a good example for us on this front are:
- Framer X
Alongside these rules I highly recommend reading the fish shell design specification, it's an incredible resource that should be applied on top of these guidelines.
Every feature should be designed for two groups of people with two flows; the "beginners" and the "power users". This means that beginners (people who haven't seen the feature before) should be able to find and use it with ease. Always ask yourself whether the feature is discoverable, not cluttered, and if the intent is clear.
Once the feature is clear and familiar for beginners, start looking into how power users can use it faster and try to educate them on this. An example of this is our dependency modal. Adding a dependency can be done with the mouse (easy for beginners), but we also hint that the user can press "enter" and the arrow keys to quickly select the dependency (power user).
Another example of this is shortcuts. It's important to clearly show users that they can use a shortcut to use the functionality that they've used often with the mouse. Superhuman does this well, they show a small popup at the button saying "You can do X by pressing Y" whenever you use your mouse to do X.
Feature discovery is tightly related to context. Showing that you can use a shortcut for an action doesn't make sense if the user hasn't used the action yet. Always ask yourself whether it makes sense to show the feature in this context. If we show all possible features and hints at the same time they will all lose meaning.
Try to make sure that your feature always works with & without the mouse. The mouse is often used when people are still unfamiliar, but after a while people will prefer to not leave the keyboard.
Everything that users can use on CodeSandbox should have defaults. Giving someone a choice before they can use something should be an absolute last resort. Start with sensible defaults and allow the user to change them later.
Asking a question to the user adds to the mental overhead someone attaches to the product. Asking too many questions will leave an impression that CodeSandbox is an app that requires a lot of thinking, and is actually more work than it saves.
It's not only mental overhead that makes us want to avoid asking questions, it's also that we're most probably breaking the user out of their flow. If they're coding and want to add a dependency, we should not ask them to add it to "dependencies" or "devDependencies". If we know that 80% of the time they want it added to dependencies we should do that by default. Asking a question will break the train of thoughts, and thus break a flow. This is also why we should always think twice before adding a modal, modals often break the current flow.
This does not mean that we shouldn't make it possible to use advanced settings. When we start with sensible defaults (preferably based on their context, like sandbox type) we should still give the option to do advanced configuration. People that want an unconventional setting are already prepared to look deeper and tackle configuration.
One of the reasons that CodeSandbox exists is because we assume the build system for the user, so they won't have to think about it unless they explicitly want to. We need to pursue this ideal to all our UX decisions.
Always try to keep the number of buttons and choices as low as possible. Every button added is another new choice for someone to think about, and most users will bail out if they have too much choice. We don't want to become Eclipse. There is a balance here to be made, for every button added the other buttons will lose a bit of meaning.
This is related to feature discoverability, we should only make a feature discoverable if it actually applies to the current scenario (context). Showing all features at all times would only serve as clutter and overwhelm (and no feature will be discovered...).
Animations are great! When used well they can help with:
- Where did this item come from?
- Did this button just change its state?
- Overall Look & Feel
- Making waiting more enjoyable
- When you need to wait for something, seeing an animation is far more fun than looking at a blank/static screen
And also important, they are fun to make! It's an artistic side to frontend development that many enjoy.
There is also the other side to this story. Animations are very pleasant in general, but if you overdo them the UI can feel more sluggish and slow. Think twice before adding an animation on "hot paths", which are actions that someone needs to do often and quickly. For example, making the hover in the file explorer a transition of background color will make the file explorer feel slow and sluggish instead of fast and snappy.
There is no rule of thumb that you can follow here, you need to feel for yourself if an animation will enrich the interaction or make it feel sluggish.
These best practices will make sure that it's easy for everyone to build and add functionality easily to the code base. The goal here is to have a fast turnaround time and quick experimentation, if you want to test something by adding a new feature that should be possible with minimal outside help.
The boy scouts have a rule: “Always leave the campground cleaner than you found it.” If you find a mess on the ground, you clean it up regardless of who might have made it. You intentionally improve the environment for the next group of campers.
The same is applicable to software development. Try to leave code in a better state than it was when you worked on it. There is no shame into fixing someone else's code, which brings me to the next point.
All the code that you write is shared by everyone, and this applies to everyone. There is no code ownership. Whenever you see a bug in code that you haven't written, or you want a new feature in an unfamiliar codebase, don't hesitate to work on that code and open a pull request!
If the change is very big, or you don't understand why the code is written the way it's written, also don't hesitate to ask the last author of the code before getting your hands dirty.
Whenever you start working on a new feature, think about what the smallest thing is that you can build and deploy. Build that first and see how people use it. Don't overengineer and think of how people will possibly use it, try to build a basic version first and deploy it. Based on the usage you will know how people actually use it.
This doesn't mean that you shouldn't polish your MVP. Don't build something that's not polished. There is a difference between the size of a feature and how polished it is. People won't use an unpolished feature as much as a polished feature, the results from their usage will be skewed.
There are multiple examples where we applied this in CodeSandbox:
import(). Based on the usage of the basic bundler we were able to prioritize and design a new bundler that supported more options.
- The editor: the initial editor didn't have support for basic things like tabs, linting, autocomplete, type checking. Based on the response from the users we knew what to add first. We were able to prioritize based on feature requests.
Remember that you only have to design your feature for your current requirement set. There's no need to overengineer and predict how people could possibly use the feature. This is a waste of time and resources, and will often result in a development block.
There is a huge cost to doing a refactor. It's a time sink, because all code that's rewritten needs to be reviewed and tested thoroughly for regressions. Even if it's tested thoroughly there's still a chance that bugs seep into the new refactor. Additionally, it's a source of merge conflicts for other people working on the same code. Automated testing and types help here, but you can't assume that these cover everything. Avoid full refactors, unless it's absolutely needed.
Because of the huge cost, it's often not wise to do a refactor. The only time we should do refactors is when the benefits outweigh the costs significantly. Think of benefits like:
- Performance increase
- Much, but really MUCH better code readability (code re-use & improved onboarding)
- New enabled functionality/flexibility
If the benefits outweigh the costs significantly; try to keep the refactors as small as possible. Follow the MVP approach and never rewrite a huge part of the codebase all at once, this will allow us to keep the costs as low as possible.
Of course, refactors need to happen from time to time. Refactors make a lot of sense if you are already working on the code you're planning to refactor, we can refer to the Boy Scout Rule again with this. Because you are planning on testing the functionality anyway, the cost of doing a refactor is significantly lower if you're refactoring the code that you're already working on.
These are my most important best practices, I'm also curious about what you find important. What could be added to this list?