# Invisible Details > A course on the art of predictable interfaces — the invisible decisions that make interfaces feel intentional, safe and obvious. A paid course ($145 one-time, lifetime access) for designers and engineers who care about interface quality. Two chapters: Notes (the ideas) and Practice (real UI components broken down decision by decision). Regional discounts apply automatically; team licences are available. The full curriculum is listed below; the three free preview lessons follow in full. The remaining lessons are paid content and are not included. ## Curriculum - Foundation: Introduction, Learning to notice, Small details that add up, Predictability is a feature - Designing Interface Behavior: Designing States, Feedback is a conversation, Forgiving Input, Interfaces should remember - Interaction Feel & Usability: Speed is a feeling, The weight of a click, Designing for Keyboard - Language & Complexity: Words shape expectations, WYSIWYG editor is hard - Foundation: Introduction - Basic UI Patterns: Stepper, Tree, Action modal - Data Visualization: GitHub activity chart, Charts - Input & Selection: Date picker ## Learning to notice (free lesson, canonical URL: https://invisibledetails.com/preview/learning-to-notice, markdown: https://invisibledetails.com/preview/learning-to-notice.md) # Learning to notice Most problems in interfaces are not difficult to solve. The difficult part is to see them at all. The fix is usually a few lines of CSS, a state that nobody handled or another transition value. So **the skill this lesson is about is noticing** - and writing the problem down in a way that will still be clear to you days later, when you already forgot which screen it even was. ## The hesitation test **The easiest way to debug things is by asking questions to yourself.** Go through some action in the product. If at any point you paused for half a second or got confused - there is a problem in that place. You paused because the screen did something you didn't expect. A button reacted strangely, a menu appeared in a wrong place or suddenly you weren't sure what the next click will do, while the rest of the app was perfectly clear. These moments are easy to ignore - your mind writes them off as distraction. Every single one points at something real. **The second click.** You clicked a button, nothing changed on the screen and you clicked one more time. In most cases the interface simply didn't confirm the first click fast enough - the loading state is missing or the transition starts too late. The worst case is when the hit target sits a few pixels away from the visible button and your first click hit nothing at all. The user has no way to know if anything registered, so they keep clicking until the screen finally reacts. **Cursor drift.** Your mouse moves around the screen, hovers over things but never actually lands. Sometimes it's a hit target problem - the clickable area is too small or it's not clear what's clickable. Sometimes it's a recognition problem - you're scanning a context menu for an option you know exists but your eyes can't find it. **Undoes that are reflexive.** You performed an action, either toggling a switch, moving a row or deleting something and then right after you performed the undo key stroke `Mod+Z` or undo button click. Not because there was anything wrong with the action, but rather because there wasn't really any feedback on what had been done and you were just slightly concerned that something might be off. **Re-reading.** Read a label, then read it again. The first pass didn't land. Almost always a wording problem. Easy to dismiss this stuff as your own quirks. It isn't. If you hesitated in a spot, real users hesitate in the same spot - they just won't tell you about it, ever. ## The “something's off” feeling Not everything catches your attention. There are subtle cues that say to you that everything seems all right but something is a little off. You can't put your finger on what that is, but you know it when you see it. And most of the time it's an issue of precision, which, in this case, doesn't have anything to do with being absolutely precise with every pixel. **Alignment.** The icons in the sidebar are not precisely aligned vertically; a label is one pixel above another. The spacing between a few controls is inconsistent. Nothing's broken, but no one cared enough to check. **Spacing.** Buttons where padding looks incorrect since text alignment is technically aligned correctly, but doesn't look visually aligned. Grid of cards where spacing between cards is different from internal padding. Modals with uneven padding - tight at the top and loose at the bottom. **Hidden problems of defaults.** Selectable text where you shouldn't be able to select it, such as drag handles, button texts and icons used only for decoration. Phone fields that pop up the complete keyboard, as opposed to the numeric one. A scrollable list without an indication that there is more underneath it. None of these problems will be noticeable when you use the product under normal circumstances. You must deliberately slow down. Check out the layout space. Attempt to select the text you shouldn't be able to select. Resize the browser window and observe which elements fail first. Go to the bottom of a list and find out whether there is anything at all there. ## Recreate friction on purpose Testing interfaces in ideal situations is what leads to this problem. Fast WIFI. Minimal content. The straightforward journey. A mouse and a 27-inch screen with test data in three rows. It's the testing in isolation, but reality is different from this, very different. People copy and paste. They type on their keyboards. They access the application on a moving train, an old phone and even on poor internet connection. The components that fail in such stressful scenarios happen to be the critical ones, but this only comes up when you intentionally put things into stress, which most people don't since it is tedious and time-consuming. > Make them even harder on purpose. **Introduce delay.** Make your network slower, so that it works at a 3G level or add some artificial delay to your API requests. All the states missing in loading animations become instantly apparent. A button not doing anything for around 800ms after being clicked by the user. As you can tell, if your app responds with a delay, users will experience it much more intensely because they're trying to get something done, and you're getting in their way. **Go for long content.** Change all those placeholders for realistic texts. A 40-character-long username without any spaces. Text in a language with words larger than those of English. An eight-digit number instead of three digits you assumed it would be. Such actions would easily uncover all the issues related to truncations or anything else. Layouts and content placement would become problematic, too. And who can say that the use of dummy content for testing did not do a lot of damage? **Clean the place out.** Get rid of all content. Nothing should be on the screen - no items, no search results, no notifications, nothing. What do you see now? The screen is intentionally blank? Or did the site simply crash? An empty screen without any text seems broken. An empty screen with one line and a button on it seems intentional. **Make things fall apart.** Fail an API request. Return a 500 error. Disconnect from the WebSocket connection. Observe carefully what happens to the user interaction with your interface in such circumstances. In particular, observe how well the application is informing users about problems and whether the application is trying to cover up problems at all costs. **Pay special attention to forms** because any bugs with clearing the form in case of failed submission result in actual support requests from frustrated users who entered a very long message and then lost it. It's like the 150-question test which you almost completed and on the last question you accidentally refreshed the page and now you need to start over. Not fun, right? **Leave the mouse alone.** Interact with your product only via the keyboard. Activate dropdowns. Select buttons. Cancel operations. Close any opened panels. Focus problems, hidden focus navigation and focus traps can be found easily here. Remember that not all of your users will be able to use mouse for interactions. **Narrow the page down.** Resize your browser window down to phone dimensions. Don't check mobile as a concept, check how stuff intended to be used on a desktop behaves in a mobile viewport. I disagree that you should build responsive only for exact screen queries (like sm, md, lg, xl), what if your user has weird screen size between md and lg? Fully responsive design is very time consuming, but in that case you can be sure that every possible screen size is covered. **Test it on a physical device.** The mobile preview of Chrome Developer Tools is not your best bet. A touch target optimized for preview works differently when you walk around with your mobile phone. A form input that resizes text to 16 pixels to avoid zooming-in on iOS Safari. A video which plays automatically and takes over the whole page. There's more between a responsive preview and your mobile device than meets the eye. **Copy-paste rather than type.** Try copying a phone number with spaces into a phone field. Copy an RGB code into a color input which accepts only hex codes. Try copying a URL with utm parameters into a URL input field. It will soon become clear whether your form fields are forgiving or punishing toward the end-user. ## What to write down The first step is noticing. The second step is making use of it correctly. Observation typically fails between the two because once you stop looking at the friction, that exact form disappears, leaving you with just "this sidebar is not right" information that is meaningless after three weeks when you don't remember which sidebar. You notice something odd, take note of the following three items: 1) **Break** - what happened: "I entered an RGB value and it was refused by the picker. I had to enter it as hex code for it to take the value." It's critical to be specific in your observation about what part of the UI failed to live up to your expectations. 2) **Expectation** - what you thought should have happened: "I expected the picker to accept a color value of `rgb(0,0,0)` even if it stores its values in hex behind the scenes." This is the important bit that most people overlook. It is the part that specifies the proper response and not simply a list of flaws. 3) **Direction fix** - the general sense of the fix. It's not a specification. One line only: "Accept any common color format and normalize it to hex." You'll be able to work with it later, even if you can't remember the context. The format is irrelevant. It could be an email, ticket, document or voicemail. But the structure is crucial. The issue, the intended outcome, and the location of the fix are key. Lose one of those and your note will be too vague for implementation. I personally use Slack DMs with myself to keep track of these things and this list is always growing. ## Build a small pattern library With experience, you notice what works instead of what fails. You see a sidebar whose icons, labels and controls are aligned identically across every section. You spot a table that uses tabular numbers for consistent alignment. You discover a scrollable menu in which the last item is deliberately clipped, allowing you to see there are more options underneath. Patterns like these deserve to be collected. However, you must only collect what deserves being collected - the easy move is to collect screenshots. Screenshots capture the look of something, which will not translate. > What you need to note is the rule, the idea underlying a component. Describe components' function, not their appearance. - “Color picker accepts any format of input and converts it into hex” is the information you will need in the next project. - “Color picker features a nice-looking gradient” will do no good to anyone. - “All icons within the sidebar appear aligned along the vertical line regardless of the current section” is a useful piece of information that can be applied to any navigational feature. - “The sidebar looks clean” is useless and cannot be re-used. Also, pay attention to minor patterns, such as `tabular-nums` in tables' data columns, `user-select: none` for decorative items, touch targets bigger than the visible icon, `text-wrap: pretty` for headings or non-breaking spaces between numbers and units. These rules are so subtle that no one ever thinks of putting them into a list. Keep the list small. Patterns do not have to be stored in a huge Notion database or a set of well-tagged Figma files. A few notes with screenshots can be sufficient here. All you really need is something to refer to in case of doubt. Update the library from time to time, some patterns that seemed innovative to you back then might become obsolete over time and get pruned from your library. ## A small habit Choose an application you admire for its quality and use extensively. Use it slowly and deliberately, not for productivity but just to observe. For me, it is Linear. I love them. I can play with their small things for hours. The moment you notice such details in applications created by other developers, you will start observing such features in yours. ## Small details that add up (free lesson, canonical URL: https://invisibledetails.com/preview/small-details-that-add-up, markdown: https://invisibledetails.com/preview/small-details-that-add-up.md) # Small details that add up None of these details matter alone. A nested border radius that's too round for its container. An icon that's technically centered but looks off. A focus ring that doesn't match the button shape. Each in isolation is invisible. As a collection, they mark the difference between a thoughtful interface and a sloppy one. Nobody judges an interface by checking every detail. Instead, a person intuitively knows whether an interface is well-polished or lacks it. This comes through the many choices people often overlook, which together produce an overall aesthetic. **What stands out when a detail is missing isn't criticism** - it's the quiet sense that if this got missed, others probably did too. The details presented here aren't facts to learn. They're illustrations of a certain level of attention to detail in design, asking what can possibly go wrong regarding spacing and default behavior, among other considerations. It's this kind of attention to detail that once developed will cause you to see these same problems everywhere else. ## Geometry that looks right The design guidelines for rounded corners, borders, padding and alignment are not always clear, but you will know them when you see them being done incorrectly. Nested border radii. When one rounded object sits inside another, the radius of the inner object needs to be smaller than that of the outer object. Here's a good guideline for choosing the radius of the inner object: inner radius = outer radius - gap between objects. If the gap is larger than the radius of the outer object, you don't need any rounding at all. When the two radii are identical, the object will appear too round to be nested inside. If you need a more detailed formula for the edge cases, there is [a very nice explanation](https://x.com/steveruizok/status/2072651350719058088) written by Steve Ruiz, the creator of tldraw. ## Optical alignment vs. mathematical alignment. Centering a play icon or checkmark inside a button with equal padding may look misaligned due to the lack of visual symmetry among icons. The play icon will look best slightly moved to the right, while the checkmark will need a tiny move down. ## Type that behaves Text is by far the most common element in every user interface and breaks down in numerous ways. Numbers in tables should be represented using monospace or tabular figures. When such numbers are not used, digits shift depending on their value because digits such as 1 and 4 take up different amounts of space. So, a change from $1.00 to $4.00 in price would move the entire column. **Text wrapping.** Text wrapping is automatic, but how well depends on what we do to it. Setting `text-wrap: pretty` ensures that no single word can end up alone at the end of a paragraph. `text-wrap: balance` does an even better job, distributing lines evenly - useful when centering headings. However, using the `text-wrap: balance` rule could create a column with even lines. In such a case, it's better to use the `max-width` rule. This will result in text that is wrapped normally, not creating columns but maintaining its paragraph-like appearance. **Truncation.** Three types of truncation can be implemented when the text doesn't fit: wrapping, end truncation or middle truncation. Default behavior (wrapping) is safe to use for any content. End truncation suits labels and descriptions where the beginning is crucial. Middle truncation is used for filenames and URLs, where the ending is more valuable - e.g., every screenshot starts with the same words, so `Screenshot 2026-…` looks identical for all of them, while `Screensho…at 14.32.08.png` still tells you which one it is. **Icon to text.** In case there's an icon next to a text that splits into two or three lines, using `align-items: center` will make the icon sit in the middle of that block, which looks unnatural. It's recommended to set a wrapper with `height: 1lh` for the icon and position it inside the wrapper to ensure it stays in the top part of the icon, as your eyes are supposed to see it there. ## Numbers and units Numbers need precision. Mistakes in that field are both misleading and unattractive. **Formatting.** It's recommended to use separators when formatting large numbers: `1000000` is less clear than `1,000,000`. Decide on whether your website will use comma separators, space separators or some other approach (based on the current locale) and follow it consistently. **Rounding.** It is unnecessary to display additional digits if they don't add any value. E.g., in a general dashboard, showing 52.3847% is pointless and misleading - 52.4% or 52% would be enough. However, showing 10.30 in a billing screen is precise enough, while 10.3 would be misleading. **Glued terms.** Never separate glued terms consisting of values and their unit. 10MB cannot split into 10 at the end of one line and MB at the beginning of the following line. Glued terms include keyboard shortcut combinations `Mod+K`, compound nouns (e.g., United States) and version numbers. ## Hit areas and touch targets A button that's fine for a mouse can be too small to hit reliably on a touchscreen. There are recommendations about making your target elements 44×44px tall and wide. But the user-visible element doesn't need to take up the full space - padding or even additional UI elements may increase the actual tappable area. But you should be wary of the proximity of targets. When two clickable elements are close to each other, users can unintentionally click them because they didn't touch the desired one. So leaving a small gap, like 4–8 pixels, between elements, makes misclicks less frequent. **Clickable labels.** When a checkbox has a descriptive label, the label should be a part of the clickable area as well. If there's a gap between the checkbox and the label, that gap should also be clickable - dead zones between related elements feel broken and people click the gap constantly. ## Invisible defaults However, there are also restrictions that are about what is not visible to the user. - `user-select: none` for icons, SVGs, decorative graphics and other purely visual elements. Without it, accidentally selecting them creates visual noise - especially during drag interactions or rapid clicking. - Input type. A phone number should open the numeric keyboard, not the full QWERTY layout. Email address input field will display `@` key, a URL will display the .com entry field. It is easy to implement with `type="tel"`, `type="email"` or `inputmode="numeric"` attributes and allows avoiding conflicts with the user. - No zooming on mobile input fields. If a font size in the input fields on iOS Safari is smaller than 16px, focusing on an input triggers a zoom-in action that can not be turned off. Set min `font-size` to 16px on all input elements and the problem goes away. Some suggestions recommend setting `maximum-scale=1` to solve the issue, but it will disable zoom for users who need it. Mobile auto play of videos. In the case of using Safari browser in an iPhone device, if you have a video with `autoplay`, it would automatically appear in fullscreen mode. However, when you use `playsinline`, the video remains in the page. ## Scroll and overflow hints Scrolling capabilities should be communicated through the interface design by removing the use of scrollbar elements. **Items only partially shown.** Scrollable lists or menus should have their containing element sized in such a way that the last visible item is cut off. It serves as an indicator that there is still more to see. If the list height perfectly coincides with the end of the item, scrolling will cease. **Background continuity.** On the iOS platform, in case of an overscroll on any particular page, you can notice the color of the background of the content. However, in case this color does not align with the page's background, there is a noticeable band of stripes on the top/bottom of the page. **Overflow gradients.** An easy gradient at the end of the scroll zone means that additional content exists. It performs well in horizontal scroll zones (tabs, sliders and tables), as the scrollbar is usually invisible in those cases. ## Micro consistency They are not rules that are separate, but rather guidelines that ensure the consistency of the entire product. **Vertical rhythm for sidebars:** All icons, labels and controls within the sidebar must align vertically to the same vertical baseline. If icons in one section have a font size of 12px and in another, 16px, then the text aligning to them would shift accordingly. **Submenu alignment.** When the menu item triggers the drop-down, the very first item in the submenu should align at the exact same height as the trigger itself. No higher, no lower. It is a minor detail indeed, yet it prevents the user's eye from being distracted. Even a shift of several pixels would make switching between the levels awkward. Consistent patterns mean consistent behavior. If some dropdown menu appears below in one location, it should be the same way in all other locations. If some input field validates on blur event in one place, all other input fields should follow the same pattern as well. Inconsistent interaction among similar product elements makes users feel like using multiple products. ## The compound effect There is no single design detail on the entire page which would ruin the product's performance. There is nothing wrong with leaving the non-breaking space or rounding a border slightly wrong. However, a user might decide not to use the product if it feels unpleasant in general. What is crucial in this context is that minor details do not attract users' attention. They accumulate either positively or negatively, making the product feel right or wrong, without users knowing why. It is a simple matter of getting the details right or having them missed. The products users consider to be done well usually have something more than a solution implemented for the core feature. The difference is not in fixing a particular issue but in a continuous quest for additional fixes. ## Stepper (free lesson, canonical URL: https://invisibledetails.com/preview/stepper, markdown: https://invisibledetails.com/preview/stepper.md) # Stepper The stepper comes into play when there is just too much to handle all at once. It may appear when you start the process of onboarding, during a guided tour through settings, while checking out or working through a form split across several pages. A long form is scary. Ten fields on one screen feel like too much. Split them into four small steps and each one feels easy. It is the same form - you just see less of it at a time. That is the whole point of a stepper. > It makes a big task feel small. The same trick works in the code. Just like the form is split into steps, the component is split into small parts - that is what a compound component is. The hard logic - focus, the active step, the animations - stays hidden, so you never touch it. You just put together a few simple pieces: a root, the step indicators and the content cards. ## Telling you which way you're going The first thing I want to talk through is direction. When the user moves forward, the panel slides up from the bottom. When they move back, it comes down from the top. Also, the title moves the same way. Although this little detail won't make anyone say "nice animation", it gives the user an answer on what they are actually doing: am I moving deeper into this folder or going out of it? Of course, without these nuances the animation still works smoothly. It just stops telling the user which way they are going. You might ask why the panels move up and down instead of left and right. There is no single answer - it comes down to your layout. If the stepper sits on the left of the page with a product illustration on the right, your eye is already moving left to right across that layout, so the content should slide sideways too - up and down would feel off. But if the page is just the stepper on its own, there is no strong pull either way: up-down or left-right both feel fine. ## Moving without a mouse Since a stepper consists of controls, there should be just one `Tab` stop instead of many. You simply `Tab` to the current step and then the arrows take over. The `Left` and `Right` arrows do the obvious thing - move to the previous or next step. I handle `Up` and `Down` too: `Up` goes to the next step like `Right` and `Down` to the previous one like `Left`. That way a vertical arrow still does something instead of nothing. Then `Home` and `End` jump straight to the first and last step, the way they do everywhere else. Steps that are disabled at the time are bypassed when navigating the stepper. That way, the user doesn't get stuck on a disabled step. ## Bigger than they look The buttons that switch steps are 20×20px, which means that even using a mouse may be a challenge and clicking these elements on a smartphone is practically impossible. It is easy to miss the step you meant to press. To fix that we can increase the clickable area without changing how it looks. A pseudo-element with a negative `inset` styles can increase the clickable region past the visible square - the step still looks like a small 20px dot, but it catches presses from a wider zone around it. The one rule is on the x axis: the zones must not overlap. If two neighbouring steps share hit area, a press near the edge is ambiguous and the wrong step fires - that confuses the user more than a small target ever would. So the horizontal growth stops right at the gap between steps. Vertically there is nothing to bump into, so it can grow more. The result is that 20×20px becomes 26×36px and all that area is clickable. Besides, gaps between the steps, which were dead zone before, are now assigned to the react to the nearest step button. ## Saying it out loud A screen reader doesn't see the slide or the highlight, so all of that has to be said out loud instead - and this is where most stepper implementations break down. Each step has an `aria-label` so it describes itself in terms other than "button". The current step is marked with `aria-current="step"`. That is the key part: it tells the user which step they are on, not just that steps exist. The disabled steps declare themselves as such rather than remaining gray and silent. That is also why I don't hang a tooltip on each step. A tooltip displays when you hover over an element, but that means it is only useful for a mouse user. Both the keyboard user and the screen reader user get no value from it. ## Turning motion off Some people turn on reduce motion in their system settings. We respect that - a setting the user chose should always win over ours. So when `prefers-reduced-motion` is on, I turn off everything: the slides, the label, the color changes, even the hover. You can't always drop every animation - sometimes the motion actually means something. But the stepper is simple, nothing here depends on the movement, so all of it can go. It still works the same, it just snaps instead of gliding. ## The fast-clicker case This is another small detail that starts out as an annoyance and turns into a tiny feature. When the steps switch faster than about 120ms apart, I drop the animation for that change, because otherwise the slides pile up on each other and the whole thing stutters. It is rare, but it happens. If the button never moves and some steps are optional, the user can just click through them fast - one change right after another. That is the case this handles: while they click fast, the animation drops out, so nothing stutters. None of this shows up in a screenshot. It shows up on the third click, when the value lands exactly where the user expected it to land. **How much someone trusts the form underneath always depends on how the steps behave.** ## Links - [llms.txt](https://invisibledetails.com/llms.txt): the index version of this file - [Homepage](https://invisibledetails.com/index.md): full course overview, pricing and FAQ - [Team licences](https://invisibledetails.com/teams.md): volume pricing for teams - [Changelog](https://invisibledetails.com/changelog.md): course updates and new lessons ## Contact - Author: Dmytro (https://x.com/pqoqubbw) - Support: support@invisibledetails.com