Field Guide · March 2026

Interface Tuning.

A field guide to thirty‑one interface details from shipping products. Every one demoed. Every one installable. Observations, not claims.

Great interfaces are rarely the product of a single brilliant decision. They are the product of thirty quiet ones, none of which the user will ever articulate, but all of which they can feel.

When a product feels right, the instinct is to credit the big things: the typography, the color system, the layout. Those are real. But they rarely account for the gap between a fine interface and one that users describe as so clean, or so snappy, or so good. That gap is almost always in the details.

This is a curated study of thirty-one such details. Every one of them is visible, right now, in a shipping product built by a team whose work you already respect. Linear, Stripe, Vercel, Raycast, Apple, Figma, Arc, Notion, Superhuman, Framer. Wherever I could cite a specific feature you can open and see for yourself, I did. Where I could not back a claim with a real product, I dropped it.

The point is not to collect tricks. It is to build the taste for noticing. Once you see how a toast pauses on hover, or how a spinner is replaced with a skeleton shaped like the data it is waiting for, or how a play button is nudged two pixels right of geometric center, you will start to see the absence of those moves in the work around you. And then, mostly, in your own.

Read straight through, or jump between sections. Each section is independent. At the end, a few words on why details compound.

ITypography

The layer where the interface stops being geometry and starts being language. Three details that separate typography that is present from typography that is tuned.

Detail 01

Tabular numerals everywhere numbers change.

The digit 1 is narrower than the digit 8 in every well-designed proportional font. That is correct for reading prose and wrong for reading columns. Anywhere numbers are expected to change, align, or compare, they should occupy equal-width slots.

The effect of getting this wrong is almost invisible in isolation and deeply irritating in aggregate. Rows with changing counts shimmer. Columns of prices drift. Timers twitch every second. All of that disappears with a single declaration.

Proportional
111
Left edge drifts
Tabular
111
Edge locked
Click Next tick to advance one step. The counters go 111 → 108 → 118 → 110 → 101 → 111. Watch the leftmost digit.
In the wild
  • Linear's issue list. Counts in the sidebar (issues per project, per cycle) are locked to tabular figures, so the layout never shifts when an issue is checked off and the count ticks down.
  • Stripe Dashboard's payments table. Amounts align to the decimal point across the entire list because the typeface runs with tnum feature settings.
  • Vercel Dashboard deployment list. Build durations (1m 23s, 42s) stay rock-steady as numbers tick live during a deploy.
How. Set font-variant-numeric: tabular-nums on any element that displays counts, timers, prices, coordinates, or statistics. If your typeface supports it, pair with slashed-zero to kill zero-versus-O ambiguity.
Detail 02

text-wrap: balance for titles, pretty for prose.

A headline that ends with a single word orphaned on its own line reads as broken layout. Historically this was fixed with manual <br> tags, which inevitably broke the moment someone shipped a new breakpoint. The browser now does it correctly, at the line-break layer, across any viewport.

wrap

Designing interfaces that feel natural and intuitive

Great design is invisible. It guides users without them ever noticing.

balance

Designing interfaces that feel natural and intuitive

Great design is invisible. It guides users without them ever noticing.

230px
Drag Column width. At most widths the left column strands a trailing word. The right column with text-wrap: balance stays even across the full range.
In the wild
  • Vercel's marketing and changelog pages. Hero and section headings stay balanced across every viewport size.
  • Stripe Docs page titles. Long doc titles like Accept a payment with a custom checkout flow never strand a word on a final short line.
  • Linear's feature pages. Section headlines are always visually even, even when the content under them shifts.
How. text-wrap: balance on headings. text-wrap: pretty on body prose (also guards against orphans but costs a little more to compute, so reserve it for paragraphs you care about).
Detail 03

Font smoothing tuned to the medium.

Default font rendering on macOS is subpixel-antialiased and tends to render a quarter-stroke heavier than the type designer intended, especially for light-on-dark. Grayscale antialiasing makes glyphs look the way the designer drew them.

This is the single most-seen and least-understood CSS declaration in shipping design systems. Apply it once at the body level and every piece of type in the product gets cleaner.

In the wild
  • Linear, Vercel, Raycast, Framer, and Arc. Every one of them ships with -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale. It is not a coincidence; it is the baseline.
  • Apple's own web properties. Developer.apple.com and apple.com use the same pair on body.
How.
body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}
Do not set it globally on icons or on very small type where the subpixel rendering actually helps.

IIGeometry and space

Before a user reads a single word, their eye does geometry. These three rules decide whether the geometry is correct.

Detail 04

Concentric border radius.

When a rounded rectangle is nested inside another rounded rectangle, the inner radius must equal the outer radius minus the gap between them. If the outer card is 16px with 12px of padding, the inner element is 4px. Anything else creates non-parallel curves that the eye catches even when the mind does not.

This rule sounds like a suggestion and is actually a constant. Once the eye is trained on it, broken radii are the easiest interface bug to spot.

Inner = Outer
18px
18px
Curves collide
Outer − Padding
18px
4px
Curves parallel
Live preview
Outer 16Pad 12 = Inner 4
16
12
4
Top: two static examples, same outer and padding. Bottom: drag the sliders. Notice how the curves fight the moment Inner drifts from Outer − Padding.
In the wild
  • Apple's iOS home screen. App icons inside folders and inside the dock track the container's superellipse so every curve is parallel.
  • Raycast's command list. The row corners, the window corners, and the modifier-key badges are all on the same center.
  • Linear's cards and panels. Nested modal panels consistently reduce radius in proportion to their padding.
How. Treat radius and padding as linked tokens. inner-radius = outer-radius − padding. If you are in a Figma-native team, bind this in variables. If you are in code, this is a two-line helper.
Detail 05

Optical alignment, not geometric alignment.

Asymmetric glyphs like a play triangle or an arrow look wrong when centered by bounding box. The triangle's visual mass is on its left edge, so geometric centering makes the shape feel pulled toward the left wall. Centering by eye, nudging one or two pixels right, is the fix. It is the one-pixel difference that elevates ordinary playback controls to Apple's.

Adjust offset
Geometric center. Feels pulled left.
+0.0px
Drag the slider or jump to presets. Between +1.5 and +2 px the triangle clicks into balance. Before that it leans. After it lists right.
In the wild
  • Apple's play button in Music, QuickTime, and the iOS media controls. The triangle is offset right of geometric center.
  • Raycast's action icons. Chevrons, arrows, and asymmetric glyphs in the action menu are baked with intentional visual padding.
  • Figma's toolbar. Glyphs are aligned optically within their hit targets, never centered to raw bounds.
How. Bake the offset into the SVG asset (intentional padding inside the viewBox), not into flexbox. If you adjust in code, it is a per-icon transform: translateX(0.5px). Never do it on the container.
Detail 06

Pixel snapping and GPU compositing.

Animating width, height, top, or left triggers layout on every frame and renders at subpixel positions, which makes text look blurry the entire time the animation is running. Animating transform and opacity runs on the compositor, skips layout entirely, and stays crisp. At rest, snap back to integer pixels so idle type is sharp.

In the wild
  • Linear's modal and panel animations. Pure translate3d on the GPU. The text inside never blurs during a slide.
  • Arc Browser's sidebar collapse. Transform-based; the page is never repainted during the animation.
  • Apple.com product pages. Parallax and hero reveals are transform-and-opacity only.
How. Use transform and opacity for motion. Avoid width/height/top/left. Add will-change: transform before the animation and remove it after. Snap idle text to integer pixels by ensuring no ancestor has a fractional transform at rest.

IIIMotion

Motion is the thinnest layer between the user's intent and the interface's acknowledgment. Five rules that decide whether that acknowledgment feels like a product or a prototype.

Detail 07

Spring physics for transforms. Eased curves for fades.

Cubic-bezier is the right tool for opacity and color, where there is no physical metaphor. For anything that moves through space, use a spring. Springs overshoot subtly and settle, which is what real objects do. It is the difference between motion that has been stamped and motion that feels lived-in.

Cubic-bezier
Spring (back-out)
Click each Move button. The spring overshoots past its target and settles back. The bezier just arrives.
In the wild
  • Linear's command menu and right panels. Open and close motion is a spring with the smallest possible overshoot. Watch the trailing edge.
  • Arc Browser's tab transitions and Easel. Tab pinning, sidebar collapse, workspace switching: all spring.
  • Framer Motion's own canvas. Every drag, drop, and snap uses their own spring engine.
  • iOS rubber-band scroll. The canonical example. The system the rest of the industry is modeled on.
How. In Motion or Framer Motion: transition={{ type: 'spring', stiffness: 400, damping: 30 }}. On Apple platforms: UISpringTimingParameters or SwiftUI's .spring(response:dampingFraction:). Start with stiffness around 300 to 500 and damping around 25 to 35 and tune by feel.
Detail 08

Animations you can interrupt at any time.

If a user clicks again mid-animation, the new transition should start from the current position and current velocity, not snap back and replay from zero. Queued or restarting animations feel laggy and unresponsive. Interruptible motion feels like the interface is tracking your input in real time, because it is.

In the wild
  • Linear's sidebar toggle. Rapidly toggling the sidebar reverses from wherever the panel happens to be. It never snaps.
  • Raycast's command window. The window resizes fluidly as you type, and every resize hands off cleanly to the next.
  • Arc's command bar. Switching modes mid-animation never produces a jarring reset.
How. Framer Motion does this by default. CSS transitions handle it automatically when transitioning the same property. Keyframe animations restart on retrigger; avoid them for state-driven motion, or reset them with a JS read of getComputedStyle before replaying.
Detail 09

Enter fast. Exit slow.

Symmetric enter and exit durations feel mechanical. Asymmetry communicates intent. Faster in than out signals I am ready, slower out than in signals you can keep reading. Pick the asymmetry that matches what the element is saying.

In the wild
  • Apple's iOS sheet presentations. Present quickly (~300ms) and settle on dismissal (~400ms).
  • Linear's toast notifications. Appear in ~150ms and fade out over ~300ms.
  • Raycast's result list. Populates fast as you type, fades out slower on clear.
How. Different duration on enter and exit. In Framer Motion: separate transition objects on initial, animate, and exit.
Detail 10

prefers-reduced-motion that actually removes motion.

Users with vestibular sensitivities get nauseous from parallax and slides even at 100ms. Reduced motion means fade-or-instant, not the same animation a little faster. This is the accessibility detail that most design systems claim to honor and fewer actually do.

In the wild
  • Apple across macOS and iOS. Every system animation has a reduced-motion variant; cross-fades replace slides throughout.
  • GitHub. Respects the setting across the product (commit graphs, PR diffs, notification drawer).
  • Stripe Docs. Reduced motion disables sidebar slide animations in favor of instant open.
How.
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
The nicer approach is to swap transforms for opacity-only variants so motion survives in a form that cannot induce discomfort.
Detail 11

View Transitions for in-page navigation.

Cross-fade or morph between views instead of hard-cutting. When the same element persists across routes (a thumbnail becoming a hero, a card becoming a detail page), animate it. Continuity tells the user this is the same thing, zoomed in, which is the strongest signal you can give that your app is coherent.

In the wild
  • Apple.com product pages. Thumbnail-to-hero morphs on click.
  • Arc Browser. Tab-to-page transitions.
  • Chrome and iOS Safari. Ship native cross-document View Transitions, used by Apple on production pages.
How.
document.startViewTransition(() => {
  // update the DOM
});
Mark the shared element with view-transition-name: hero. Falls back to a hard cut on unsupported browsers, which is fine.

IVSurfaces and depth

Two rules that decide whether a piece of UI reads as a physical object on a surface, or a flat rectangle on a page.

Detail 12

Translucent chrome with backdrop-filter.

Overlays, sidebars, and toolbars read better as blurred translucent surfaces than as opaque fills. The translucency gives layers a sense of depth, and keeps users oriented to what sits behind the panel. It is the cue that says this is floating, not replacing.

In the wild
  • macOS (Big Sur onward). Menu bar, sidebars, and Control Center. Vibrancy is the OS-level default.
  • Apple's visionOS and the 2025 Liquid Glass design language. Pushes translucency to the system aesthetic; every chrome element is a literal glass material.
  • Arc Browser's command bar. Blurred background over the page, never opaque.
  • Raycast on macOS. The entire window is a vibrancy material.
How.
backdrop-filter: blur(20px) saturate(180%);
background: rgb(255 255 255 / 0.72);
Always provide an opaque fallback with @supports not (backdrop-filter: blur(1px)). Avoid translucent chrome over video backgrounds unless you can afford the cost.
Detail 13

Border plus shadow. Rarely one without the other.

Pure borders feel flat and editorial. Pure shadows feel mushy on light backgrounds. A 1px hairline border combined with a soft low-opacity shadow reads as a physical object resting on a surface. This is the single most common recipe in shipping design systems, and almost nobody gets it right on the first try.

Live card
Track expenses, build lasting habits
Budgeting turned upside down. Focus on what matters and keep your spending intentional.
Neither. Floats nowhere.
Border
Shadow
Flip the toggles. Border alone reads editorial, shadow alone reads mushy, both together read as a physical object on a surface.
In the wild
  • Linear's cards. Issues, projects, and modals. Hairline border plus faint shadow on every surface.
  • Vercel Dashboard's deployment cards. Same recipe. Hairline plus soft shadow.
  • Stripe's pricing page cards. Identical pattern, tuned for light bg.
How.
border: 1px solid rgb(0 0 0 / 0.06);
box-shadow:
  0 1px 2px rgb(0 0 0 / 0.04),
  0 4px 12px rgb(0 0 0 / 0.04);
In dark mode, replace the border with a top-edge highlight: box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06). It reads as light catching the top of the card.

VInteraction

Five details that decide whether the user feels in control of the interface, or at the mercy of it.

Detail 14

Focus rings that belong to the brand.

Replace the browser's default outline with a designed focus ring, but never remove it. The default looks bolted on. A custom ring in the accent color signals that keyboard navigation is a first-class path, not an oversight.

Browser default Looks bolted on
Designed ring Belongs to the system
Tab into each button with your keyboard. Same button, two levels of care.
In the wild
  • Linear. Every interactive element gets a 2px brand ring with a 2px offset.
  • Vercel's Geist design system. Focus is a doubled outline, inner ring in the surface color, outer ring in the accent. Works on any background.
  • Stripe's checkout fields. Focus is a subtle ring plus a slight shadow elevation. Never just a blue outline.
How. Use :focus-visible, not :focus, so mouse clicks do not trigger the ring.
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
Detail 15

Cmd+K as the primary navigation.

A single keyboard-summoned palette that searches, navigates, and triggers actions across the entire app. Power users reach ten-times speed. The palette also doubles as documentation: every action in your product becomes searchable by name, which means your app's surface area is discoverable without a manual.

In the wild
  • Linear's Cmd+K. Jumps to issues, cycles, and projects. Triggers actions like Change status and Assign inline.
  • Raycast. The OS-level expression of the paradigm.
  • Vercel Dashboard's Cmd+K. Navigates between projects, deployments, teams, and settings.
  • Stripe Dashboard, Notion, Figma, Slack. All ship their own variant.
How. cmdk by Paco Coursey for React. Combine recent items, suggested actions, and live search into a single arrow-key-navigated list with fuzzy matching.
Detail 16

Hover intent.

Do not open submenus or tooltips on mouseenter. Wait ~150ms, and only fire if the cursor is still pointed at the target. Without intent, dragging a cursor across a nav opens five menus in sequence. With it, only the menu you settled on opens.

In the wild
  • Apple.com's mega-menu. Requires a brief settle before opening. Sliding the cursor across the top nav does not trigger any submenu until you stop.
  • Linear's nested submenus. Hover-open only after the intent threshold.
  • Notion's page hover-previews. ~300ms delay before the preview card materializes.
How. Radix UI's HoverCard handles this. Custom: setTimeout on mouseenter, clearTimeout on mouseleave, only open if the timer survives.
Detail 17

Tooltip delay, tiered by intent.

A tooltip that opens at 0ms is noise. A tooltip that always waits 500ms means scanning a toolbar takes forever. The tiered delay reads user intent: first tooltip opens after ~500ms, subsequent tooltips in the same toolbar open instantly as the cursor sweeps across.

Hover the first button and wait. Then sweep across the row. The first delays 500ms, the rest are instant.
In the wild
  • Figma's toolbar. First tooltip waits, subsequent ones along the same toolbar appear instantly.
  • Linear's icon buttons. Same pattern. Once the first tooltip is open, the others arrive instantly.
  • macOS menu bar. The identical behavior at the OS level.
How. Radix Tooltip: wrap your tree in <Tooltip.Provider delayDuration={500} skipDelayDuration={0} />. The skip-delay is the detail that makes this feel right.
Detail 18

Escape that always backs out exactly one layer.

Esc becomes a trusted get me out key only when it is never load-bearing in the wrong direction, and never ambiguous. First press closes the topmost modal. Second press closes the popover. Third press clears the search. Fourth press deselects. Never two layers at once, never destructive.

In the wild
  • Linear. Esc closes the command menu, then filter popovers, then deselects issues. Each press is predictable.
  • Figma. Esc deselects. Second press exits the current tool to the Move tool.
  • Raycast. Esc backs out one navigation level inside an extension, then closes the window.
How. An app-level focus/layer stack. The topmost overlay handles Esc and calls stopPropagation before lower layers can react. Radix's <Dialog> and <Popover> compose this correctly; prefer composing them to rolling your own.

VIFeedback and speed

The gap between snappy and laggy is almost entirely here. Four details that decide how fast your product feels, independent of how fast it is.

Detail 19

Optimistic UI commits.

Apply state changes locally before the server confirms; reconcile on response. This one detail is the difference between snappy and laggy for almost every interaction in a cloud product. The round-trip is invisible to the user. The rollback, if it comes, is rare enough to be handled with a toast.

In the wild
  • Linear. The whole product is built on this. Status changes, assignments, comments all reflect instantly; the sync happens in the background.
  • Superhuman. Archive, delete, snooze are instant. The keyboard shortcut commits before the server hears.
  • Discord. Messages render in the channel before the websocket acknowledges them.
  • Notion. Typing into a block commits immediately; conflicts resolve on the way back.
How. TanStack Query's useMutation with onMutate for optimistic updates. SWR's mutate with rollback. Keep a transient pending flag for visual feedback on long-running actions.
Detail 20

Undo as a first-class action.

Every destructive action surfaces an undo affordance for ~5 seconds before becoming permanent. Confidence comes from reversibility. Are you sure? dialogs interrupt flow. A short undo window protects without nagging.

Delete then undo
Item deleted
Click Delete. A toast offers Undo for four seconds. The timeline progress bar is the small detail users never consciously register and always expect.
In the wild
  • Gmail's Undo Send. Configurable delay up to 30 seconds; the feature that defined the category.
  • Linear. Deleting an issue or archiving a cycle shows a toast with Undo for five seconds.
  • Superhuman. Every command has Cmd+Z undo, including sent email within a configured delay window.
  • Notion. Cmd+Z undoes block deletion, page archiving, and even drag operations.
How. Enqueue the destructive operation behind a setTimeout. Render a toast with an Undo button that cancels the timer. Persist the toast across route changes so users can still recover on nav.
Detail 21

Skeleton screens shaped like the content.

While data loads, render gray placeholder rectangles that match the actual layout, not a centered spinner. Skeletons signal your data is coming, here is the shape it will take. Spinners signal something is happening, no idea what. The perceived-latency drop is measurable; the feel difference is immediate.

Loading state
D
Ship faster with live data
3 hrs ago · Vercel Cloud
Ready
Click Reload. The card morphs into a skeleton shaped like its real content, then reveals. Spinner equivalent would show a single spinning glyph in the middle of a blank card.
In the wild
  • YouTube. Thumbnail and metadata placeholders before the feed loads.
  • Linear. Issue rows render as gray bars in the exact row geometry before the data hydrates.
  • Vercel Dashboard. Deployment cards skeleton in with project-name, status, and branch shapes.
  • LinkedIn. Originated the technique at scale on the feed.
How. Match real component dimensions. Use a subtle shimmer with a linear-gradient animated over 1.5 to 2 seconds. Do not shimmer too fast. Use <Skeleton className="h-4 w-32" /> from shadcn/ui or equivalent.
Detail 22

Toasts that place predictably and pause on hover.

Toasts appear in a consistent corner, stack newest on top, auto-dismiss after ~5 seconds, and pause the timer whenever the cursor enters. Pause on hover is the small detail that means users can actually read the toast or click Undo before it vanishes.

In the wild
  • Linear's toasts. Bottom-right, swipe to dismiss, pause on hover.
  • Vercel's deploy toasts. Bottom-right with progressive status updates and Undo baked in.
  • Sonner by Emil Kowalski. Used by Vercel, Resend, and countless others. The canonical implementation.
How. Use Sonner. It handles stacking, swipe-to-dismiss, hover-pause, reduced-motion, and accessibility correctly out of the box.

VIIForms

Forms are where most products ask the most of the user, and where most products give them the least. Three details that make forms feel like a conversation instead of an interrogation.

Detail 23

Validate on blur, then live.

Validate when the user leaves a field, not on every keystroke. After the first error, switch to live validation so the user can see when they have fixed it. Errors that fire while the user is still typing their email feel accusatory. Blur-then-live feels like the system is patient.

In the wild
  • Stripe's checkout (Stripe Elements). Card number errors appear only after blur, then update live as the user corrects.
  • Apple ID signup. Password rules show live as you type; field-level errors fire on blur.
  • Linear's settings forms. Same pattern throughout the product.
How. React Hook Form: mode: 'onTouched'. Native: attach blur validation, then switch the same field to input validation once it is touched-and-invalid.
Detail 24

Paste handling that reads the clipboard.

When a user pastes a URL, formatted text, or structured data, parse it and pre-fill the fields. Do not dump the raw string into one field and expect the user to clean it up. Smart paste feels like the app is reading your mind. The opposite feels like work.

In the wild
  • Linear's issue creation. Pasting a GitHub URL auto-attaches the issue or PR with title, status, and assignee.
  • Notion's URL paste. Prompts you to paste as bookmark, embed, or plain URL, with sensible defaults by host.
  • Notion Calendar (formerly Cron). Pasting a Zoom or Meet link into the location field parses it as a meeting URL.
  • Figma's clipboard. Pasting CSS gradients or colors creates the equivalent fill.
How. Listen for the paste event. Read clipboardData for text/uri-list, text/html, JSON, or domain patterns you know how to parse. Call preventDefault and route the parsed payload into the right fields.
Detail 25

Autofocus the first useful field.

When a modal opens, the caret should already be in the field the user came to fill. Not on the label, not on the close button, not nowhere. Save a tab keystroke on every open. But do not autofocus on initial page load, where it traps screen readers and hijacks the expected scroll position.

In the wild
  • Linear's Create issue modal. Focus drops into the title field the instant the modal opens.
  • Raycast on launch. Focuses the search input, which is the right call here because it is the only useful target.
  • Notion's New page. Caret lives in the title.
How. autoFocus on the input, or imperatively inputRef.current?.focus() after mount. Pair with :focus-visible so the ring only renders for keyboard users.

VIIIState and navigation

Interfaces fail quietly the moment they forget where the user was. Four details about remembering.

Detail 26

Scroll restoration on back navigation.

When users navigate back, return them to the exact scroll position they left from. Users mentally bookmark I was here. Resetting scroll on back-nav is the single biggest reason an SPA feels worse than a traditional MPA, and the fix is almost always a one-line config.

In the wild
  • Apple.com. Deep product pages preserve exact scroll on browser back.
  • Stripe Docs. Navigating into an API reference and back keeps you where you were.
  • Notion. Bouncing between pages restores scroll on back.
How. The native browser does this with full-page navigation and back/forward cache. SPAs must opt in: Next.js App Router has experimental.scrollRestoration. React Router ships <ScrollRestoration />. For virtualized lists, persist scroll via TanStack Virtual or equivalent; they handle this case correctly.
Detail 27

Empty states with a primary action.

First-run and empty views should show what could be there plus a single button to make it happen. Never just No items yet. Empty states are the most-seen part of new-user flows. A blank panel says the product is broken or boring. A pointed CTA says here is exactly what to do.

In the wild
  • Linear's No issues state. Shows an illustration plus a Create issue CTA with the keyboard shortcut hint.
  • Notion's blank page. Ghost prompt Type / for commands with a suggested list of templates.
  • Vercel's empty project list. Import Git Repository CTA with shortcuts to common templates.
  • Figma's empty file. Surface the keyboard shortcut for rectangle, text, frame.
How. Treat empty state as a designed surface with hierarchy. Short title. One-sentence why. Primary CTA. Keyboard shortcut hint. Optional illustration. Never a blank rectangle.
Detail 28

Selection that survives re-renders.

When data refreshes, keep the user's selection, scroll position, expanded sections, and active filters intact. Few things feel worse than losing your place because a parent component re-rendered on a real-time update. Persistent UI state is invisible when it works and infuriating when it does not.

In the wild
  • Linear's issue list. Live updates from teammates do not blow away your current selection or filter.
  • Figma's layers panel. Collaboration updates do not collapse your tree expansion.
  • Notion's sidebar. Real-time page creation by others preserves your expanded pages.
How. Keep selection in a ref or external store (Zustand, Jotai) keyed by stable IDs, not by index. Use stable key props on list items. Persist scroll separately from selection.
Detail 29

Stream what you have. Let the slow data land later.

Render the shell and any cached or fast data immediately. Let slow data stream in as it arrives. Time-to-first-meaningful-paint matters more than time-to-fully-loaded. Streaming makes the product feel responsive even when individual queries are slow.

In the wild
  • Vercel's dashboard. Project metadata renders instantly; deployment status streams in via React Server Components and Suspense.
  • Linear's app. Issue list paints from local cache first; server reconciles in the background.
  • v0.dev. Generated code streams token-by-token. The chrome is interactive immediately.
  • ChatGPT and Claude. The response stream is the canonical user-facing example.
How. React Suspense with streaming SSR (Next.js App Router is the easiest path). For data, layer a local-first cache (IndexedDB via Dexie, or a sync engine modeled on Linear's) underneath network fetches.

IXThe feel of native

Two details that sit outside the DOM and show up anyway as taste.

Detail 30

Haptic feedback on touch devices.

Touch screens have no physical click. A 10ms haptic tap restores the feedback loop that your hand expects from a button. Pair micro-interactions like toggle flips, tab switches, and segmented control changes with a soft tap. Reserve heavy haptics for confirmations.

In the wild
  • iOS toggle switches and segmented controls. Every flip plays a light impact generator.
  • Apple's Camera. Heavy haptic on shutter capture, light on zoom snap.
  • Linear's iOS app. Completing an issue plays a soft tap.
  • Things 3 by Cultured Code. Checking off a todo has a satisfying haptic flick.
How. iOS: UIImpactFeedbackGenerator, UISelectionFeedbackGenerator, UINotificationFeedbackGenerator. Web: navigator.vibrate(10) (Android only; quiet failure on iOS). Reserve heavy impact for confirmations, light for selection.
Detail 31

Density modes. Comfortable for new, compact for daily.

Let power users choose tighter row heights. Default new users to comfortable. Density is a personal preference that scales with experience. New users want breathing room; daily users want fifty rows on screen. Forcing either is a tax on the other.

Display options
Display
Triage the inbox
Weekly review, Monday 9am
Plan the quarter
Retros Friday afternoon
Ship the portfolio
Toggle between modes. The same list; two rhythms. A daily user will pick compact in under a week.
In the wild
  • Linear's display options. Row density, sidebar size, theme, all adjustable per view.
  • Gmail's display density (Default, Comfortable, Compact). The canonical implementation.
  • Notion's Toggle small text. Page-level density toggle.
  • Notion Calendar. Zoom level on the day view scales row height fluidly.
How. Tokenize spacing (--row-h: 12px vs 8px) and let it cascade. Persist the user's choice in localStorage or the user record.

XInstall and ship

You read it. Now run it. Every utility, token, and guard behind the details above is shipped as a Shadcn-compatible registry, hosted on this site.

Details compound.

None of the details above is novel. Every one of them has been described before, probably better, on someone else's blog. What is rarer is the willingness to sit with thirty of them, trace each to a real product, and build the taste for noticing them in your own work.

The interfaces that feel great are rarely the result of one obvious move. They are the result of a team that got all thirty of these decisions right, and then got the next thirty right after that. This essay is a starting list. The real work is what happens after you start seeing these details everywhere, including in the gaps where you have not shipped them yet.

If you build software and you want your users to use the word feel, this is the layer to study.

Dani Asyrofi, Jakarta, March 2026.
End of field guide Back to portfolio