Details that make interfaces feel better
Great interfaces rarely come from a single thing. It's usually a collection of small things that compound into a great experience. Below are a few small details I use to make my interfaces feel better.
A quick way to improve how text behaves in your app is to use text-wrap: balance. It distributes text evenly across each line, avoiding orphaned words.
Designing interfaces that feel natural and intuitive
Great design is invisible. It guides users without them ever noticing.
Designing interfaces that feel natural and intuitive
Great design is invisible. It guides users without them ever noticing.
text-wrap: balance distributes text evenly across lines, preventing orphaned words.
You can also use text-wrap: pretty to get a similar result, but it's a bit slower because it uses a different algorithm.
Concentric offset is a technique used to create a balanced visual look when nesting elements inside one another. This is one of the more important concepts that make interfaces feel great and it often goes unnoticed.
There is a formula to calculate the correct values and it's very simple. The outer radius equals the inner radius plus the padding.
There's still a surprising number of apps and interfaces that don't do this and instead mismatch border radii. If you're not already doing this, I'd recommend starting. It will make your interfaces feel much better.
Change the values to see how the border radius adapts
Animating opacity, scale and blur on icons when they are shown contextually makes the transition feel better and more responsive.
<button onClick={handleCopy} className="button">
{isCopied ? <CheckIcon /> : <CopyIcon />}
</button>You can achieve the same effect with CSS only as well. I personally prefer Motion, because it allows me to use spring animations easily.
On macOS, text rendering can sometimes appear heavier than intended.
Subpixel rendering
Default font smoothing uses subpixel antialiasing on macOS.
Antialiased rendering
Grayscale antialiasing produces thinner, crisper light text.
Setting -webkit-font-smoothing: antialiased or just antialiased in Tailwind makes text render slightly thinner and crisper.
<html lang="en">
<body class="font-sans antialiased">
<main>
{children}
</main>
</body>
</html>
The best way to apply this is to add it to the entire layout. That way it applies to all of the text elements in your app.
If your numbers shift when they update, use font-variant-numeric: tabular-nums or just tabular-nums in Tailwind.
Click the play button to make the numbers run up
It makes the digits equal width. Keep in mind that some fonts such as Inter change the look of numerals when this property is used.
When it comes to interruptibility, CSS transitions and keyframe animations behave differently. Transitions interpolate toward the latest state and can be interrupted, while keyframe animations run on a fixed timeline and don’t retarget after they start.
Click the rotate button rapidly to see the difference
Users often change their intent mid-interaction. For example, a user may open a dropdown menu and decide they want to do something else before the animation finishes.
Try to toggle the animation again while it's running
If animations aren't interruptible, it can make the interface feel broken. For example, on iOS, interruptibility is quite prevalent for this very reason.
Interrupt the animation while it's running to see the difference
A rule of thumb that can help you decide when to use CSS transitions vs keyframe animations is that CSS transitions are great for interactions, while keyframe animations are better for staged sequences that run once.
Interrupt the animation while it's running to see the difference
Enter animations often combine opacity, blur and translateY. It helps to break the animated components into smaller chunks and animate them individually instead of animating a big block at once.
The first variant animates a single container containing the title, description, and buttons. The second variant animates the title, description and buttons individually, with a 100ms delay between each section.
<div className="animate-enter" style={{ "--stagger": 1 }}>
<Title />
</div>
<div className="animate-enter" style={{ "--stagger": 2 }}>
<Description />
</div>
<div className="animate-enter" style={{ "--stagger": 3 }}>
<Buttons />
</div>
The third variant animates the title by splitting it into individual spans. Each span contains a word and is animated individually with an 80ms delay between them.
@keyframes enter {
from {
transform: translateY(8px);
filter: blur(5px);
opacity: 0;
}
}
.animate-enter {
animation: enter 800ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
animation-delay: calc(var(--delay, 0ms) * var(--stagger, 0));
}
.animate-enter-individual-title {
--delay: 80ms;
}
The description remains a single block and the buttons are also animated individually rather than their entire container.
Exit animations usually work better when they're more subtle than enter animations.
Swap between the modes and toggle the animation to see the difference
Exiting elements don’t need the same amount of movement and attention as entering elements. In the full example, the exit y value is calc(-100% - 4px), which is the height of the container plus the padding.
<motion.div
key="menu"
className="container"
initial={{ opacity: 0, y: "calc(-100% - 4px)", filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{
opacity: 0,
y: "calc(-100% - 4px)",
filter: "blur(4px)",
}}
transition={{ type: "spring", duration: 0.45, bounce: 0 }}
/>
In the subtle mode, we use a fixed value of -12px. Some subtle motion should still remain to indicate the direction, so I wouldn't recommend removing the animation completely.
<motion.div
key="menu"
className="container"
initial={{ opacity: 0, y: "calc(-100% - 4px)", filter: "blur(4px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
exit={{
opacity: 0,
y: "-12px",
filter: "blur(4px)",
}}
transition={{ type: "spring", duration: 0.45, bounce: 0 }}
/>
By doing this, the exit animation becomes much softer, less jarring and doesn't demand the same amount of attention as the enter animation.
Aligning items geometrically works great most of the time, but there are instances where it just looks off. When that happens, it is best to align items optically instead.
For example, when a button has both text and an icon, it is better to have a slightly smaller padding on the side of the icon to optically align the content.
Change the alignment mode and click the button to see the difference. In this example only the Play icon is optically aligned.
This often happens with icons. While a lot of icon packs already account for this, there are shapes that need to be optically aligned. I usually fix it by adding margin or padding depending on the container.
Change the alignment mode and click the button to see the difference
For icons, the best way to fix it is in the svg itself, so no additional margin or padding needs to be added.
Instead of borders, I often prefer to use a subtle box-shadow, that adds more depth to the element.
The difference here is only noticeable in light mode
The shadow in this example is comprised of three different shadows.
.border-shadow {
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.06),
0px 1px 2px -1px rgba(0, 0, 0, 0.06),
0px 2px 4px 0px rgba(0, 0, 0, 0.04);
}
For the hover state, it is the same box-shadow just slightly darker. To transition between the shadows, we can add box-shadow to the transition property transition-[box-shadow].
.border-shadow {
box-shadow:
0px 0px 0px 1px rgba(0, 0, 0, 0.08),
0px 1px 2px -1px rgba(0, 0, 0, 0.08),
0px 2px 4px 0px rgba(0, 0, 0, 0.06);
}
It also helps when using images or multiple colors as backgrounds. Shadows are versatile and adapt well to any background since they use transparency.

Solid colors, on the other hand, don't work well when used on backgrounds other than the ones they were designed for.
A visual tweak I use a lot is adding a 1px black or white (depending on the mode) outline with 10% opacity to images.

This creates a sense of depth and a somewhat consistent outline around the element.
.border-overlay {
outline: 1px solid rgba(0, 0, 0, 0.1);
outline-offset: -1px;
}
.dark .border-overlay {
outline-color: rgba(255, 255, 255, 0.1);
}
I mostly use this in design systems where other elements also use borders.
More
If you have any questions you can reach me via email, see more of my work on X (Twitter) or subscribe to my newsletter below.
Newsletter
I share stuff that I'm working on, new posts and resources here.