A great interface is rarely about one big idea. More often, it’s the little things that quietly add up and make the experience feel right. Here are a few details I keep coming back to when designing interfaces.
Make text feel lighter
Typography is one of those things you notice without necessarily knowing why. On macOS, text can sometimes look a little heavier than expected, especially when you’re using lighter weights or smaller text.
A simple way to change that is to use grayscale antialiasing instead of the browser’s default font smoothing.
Interface details
The little things quietly add up and make the experience feel right.
Interface details
The little things quietly add up and make the experience feel right.
Use -webkit-font-smoothing: antialiased, or Tailwind’s antialiased class, and the type usually comes out a bit thinner and cleaner.
<html lang="en"> <body class="font-sans antialiased"> <main> {children} </main> </body></html>
The cleanest approach is to set it once on the root layout so every text element inherits it automatically.
Better text wrapping
One of the easiest upgrades for headings is text-wrap: balance. It spreads the words more evenly across each line, so titles look less lopsided.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.
text-wrap: balance spreads words evenly across lines so the last one isn’t left hanging alone.
You can also use text-wrap: pretty to keep a single word from sitting alone at the end of a sentence. It’s a quieter fix, and it works better for body text than for titles.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.
text-wrap: pretty keeps a single word from sitting alone at the end of a paragraph. Just note: Firefox doesn’t support it yet.
Most of the time, you’ll combine them: text-wrap: balance for the title, text-wrap: pretty for the body.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.
Building products that feel calm and deliberate
Good craft stays quiet. It carries people through each small careful moment.

It adds a little depth and keeps a soft, even outline around the image.
.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);}
Prefer shadows over borders
Instead of a border, I often use a soft box-shadow. It gives the element a bit more depth. You’ll notice it most in light mode, but it works in dark mode too, where the shadow almost reads like a border.
This example stacks three shadows on top of each other.
.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);}@media (prefers-color-scheme: dark) { .border-shadow { box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08); }}
On hover, use the same box-shadow, just a little darker. Add transition-[box-shadow] so the change feels smooth.
Make exits quieter
Things usually feel better when they leave more quietly than they arrive.
Toggle the animation to compare the subtle and full exits side by side.
Things leaving don’t need as much attention as things coming in. On the left, we only animate x between 0 and calc(-100% - 4px), which is the drawer’s full width plus a little padding, so it slides all the way off screen.
<motion.div key="menu" className="container" initial={{ x: "calc(-100% - 4px)" }} animate={{ x: 0 }} exit={{ x: "calc(-100% - 4px)" }} transition={{ type: "spring", duration: 0.45, bounce: 0 }}/>
In the second example, the exit doesn’t move x at all. It just fades out with opacity and filter.
<motion.div key="menu" className="container" initial={{ x: "calc(-100% - 4px)" }} animate={{ x: 0 }} exit={{ opacity: 0, filter: "blur(4px)" }} transition={{ type: "spring", duration: 0.45, bounce: 0 }}/>
That makes the exit softer and less abrupt, so it doesn’t pull as much attention as the enter.
Bring things in piece by piece
Enter animations often mix opacity, blur, and a little translateY. It usually feels better if you break the UI into smaller pieces and animate those one by one, instead of moving the whole block at once.
Try Block, Sections, and Individual to see how the same hero enters three different ways.
Block animates the title, description, and buttons as one container. Everything shares the same enter, so the whole hero arrives together.
Sections keeps those three pieces separate. The title starts first, then the description, then the buttons as a group, with a 200ms gap between each section.
<div className="animate-enter animate-enter-sections" style={{ "--stagger": 0 }}> <Title /></div><div className="animate-enter animate-enter-sections" style={{ "--stagger": 1 }}> <Description /></div><div className="animate-enter animate-enter-sections" style={{ "--stagger": 2 }}> <Buttons /></div>
Individual goes one step further on the title. Each word is its own span and enters with a 100ms delay between them. After the words, the description still enters as a single block. Then each button enters on its own, also spaced by 100ms.
@keyframes enter { from { transform: translateY(8px); filter: blur(8px); opacity: 0; }}.animate-enter { animation: enter 600ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: calc(var(--delay, 0ms) * var(--stagger, 0));}.animate-enter-sections { --delay: 200ms;}.animate-enter-individual-title { --delay: 100ms;}
So in Individual, the cascade is words, then the paragraph, then each button. The description never needs to split. The buttons do, because two separate controls feel clearer when they don’t arrive as one chunk.