Skip to content
ssntl/ui

Details

Interfaces that feel right rarely come from a single thing. They are small details that compound. Every ssntl/ui component goes through these twelve.

Based on “Details that make interfaces feel better” by Jakub Krehel (opens in a new tab)

01 · Text that wraps well

text-balance spreads a title over even lines; text-pretty keeps a paragraph from ending on a lonely word. They go together: balance on the title, pretty on the description. ssntl/ui ships them in the base layer.

wrap

Designing interfaces that feel natural and intuitive

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

Incorrect
balance

Designing interfaces that feel natural and intuitive

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

Correct
220px
text-balance spreads the title evenly across lines.
tsx
<h2 className="text-balance">…</h2>
<p className="text-pretty">…</p>

02 · Concentric radii

A surface inside another takes the outer radius minus the gap between them, so the corners stay parallel. Move the controls: on the left the radius follows the formula, on the right it repeats and the inner corner looks swollen.

padding: 8px
12px
12px
Incorrect
20px
12px
Correct
Same inner radius and padding: only the outer radius that follows the formula looks right.
outer radius=inner radius+padding
40px
24px
16px
40 = 24 + 16
tsx
<div className="rounded-outer-lg p-concentric-2">
  <div className="rounded-inner">…</div>
</div>

03 · Icons that change with context

When one icon replaces another they cross-fade with opacity, scale and blur (0.25 → 1, 4px → 0) instead of jumping. Both stay in the DOM, so the swap has an enter and an exit. The copy button on this site does it.

no animationIncorrect
opacityIncorrect
opacity + scale + blurCorrect
Click each button, or toggle all of them together.
tsx
<span className="icon-swap" data-swapped={copied}>
  <Copy />
  <Check />
</span>

04 · Crisp text

On macOS text can render heavier than designed. antialiased on the layout thins it for the whole app. The difference only shows on Mac screens.

subpixel-antialiased

Subpixel rendering

Default font smoothing uses subpixel antialiasing on macOS.

Incorrect
antialiased

Antialiased rendering

Grayscale antialiasing renders thinner, crisper text.

Correct
The difference only shows on Mac screens.
tsx
<body className="font-sans antialiased">…</body>

05 · Tabular numbers

When a number changes, its digits should be the same width so nothing shifts. Count again and watch the right side: the width doesn't move.

proportional

Q 1,248,750

Incorrect
tabular

Q 1,248,750

Correct
Count up and watch the width: tabular digits don't shift.
tsx
<span className="tabular-nums">Q 1,248,750</span>

06 · Interruptible animations

Transitions head to the latest state and can be interrupted; keyframes run on a fixed timeline and restart. Transitions for interactions, keyframes for sequences that run once. Click several times while it turns.

CSS keyframe
Incorrect
CSS transition
Correct
Click rotate rapidly to see the difference.
tsx
// Interaction: a transition retargets mid-way.
<div className="transition-[rotate] duration-(--duration-menu) ease-out" />

07 · Staggered entrances

Instead of animating one block, the title, the description and the actions enter separately, 100ms apart. A title can split into words at 80ms. For rare entrances only, never for frequent interactions.

one block

Publish your store today

Twelve products and a domain, ready.

Incorrect
in chunks · 100ms

Publish your store today

Twelve products and a domain, ready.

Correct
by words · 80ms

Publish your store today

Twelve products and a domain, ready.

Correct
Replay and compare: one block, chunks, and words.
tsx
<h2 className="animate-enter stagger-0">…</h2>
<p className="animate-enter stagger-1">…</p>
<div className="animate-enter stagger-2">…</div>

// By words: enter-words drops the step to 80ms.
<span className="inline-block animate-enter enter-words stagger-3">store</span>

08 · Subtle exits

What leaves needs less attention than what arrives. Instead of sliding its full width, the panel travels 30% and lets opacity and blur do the rest.

full exit
Incorrect
subtle exit
Correct
Close and open: the subtle exit asks for less attention.
tsx
// With Motion: the exit travels less than the entrance.
<motion.div
  initial={{ x: "calc(-100% - 4px)" }}
  animate={{ x: 0 }}
  exit={{ x: "-30%", opacity: 0, filter: "blur(4px)" }}
  transition={{ type: "spring", duration: 0.45, bounce: 0 }}
/>

09 · Optical alignment

Sometimes centering by the numbers looks wrong. A button with an icon takes 4px less padding on the icon side, and the play triangle moves slightly right because its weight sits left. Button does it with data-icon.

geometric
Incorrect
optical
Correct
The icon side gets 4px less padding; the play glyph moves 1px right.
tsx
<Button>
  <Play data-icon="inline-start" />
  Play
</Button>

10 · Shadows instead of borders

A transparent three-layer shadow adds depth and takes the color of any background; a solid border keeps its grey and looks pasted on. In dark mode it reads as a 1px border. Hover the cards.

border
Pro planQ 450 a month
Incorrect
border-shadow
Pro planQ 450 a month
Correct
On a tinted ground the border keeps its own grey; the shadow takes the color underneath.
tsx
<div className="border-shadow hover:border-shadow-strong">…</div>

11 · Image outlines

A 1px black (white in dark) outline at 10% gives images a consistent edge on any surface. Pure black or white: a tinted grey reads as dirt.

no outlineIncorrect
image-outlineCorrect
A 1px pure black or white outline gives the image a consistent edge.
tsx
<img className="image-outline" … />

12 · Type scale

When supporting text is as big as the title, the reader has nowhere to start. Map each role to a step of the system's scale: heading 22, body 17, caption 13. Hierarchy comes from size, weight and color together.

Design for a quick glance

Alex Morgan · 12 June 2026 · 4 min read

People scan a page before they read it. A clear title tells them what to expect, a short introduction adds context, and the details answer their questions.

Heading:
15px
Supporting:
17px
Paragraph:
17px
Each role gets its own size: the title leads, the metadata steps back.
tsx
<h3 className="text-heading">Design for a quick glance</h3>
<p className="text-caption text-muted-foreground">Alex Morgan · 12 June 2026</p>
<p className="text-body text-muted-foreground">…</p>

Install the utilities

border-shadow, image-outline, icon-swap and animate-enter come in one file. Concentric radii have their own.

app/globals.css
/* npx shadcn@latest add @ssntl/details @ssntl/radius-concentric */
@import "../styles/ssntl/details.css";
@import "../styles/ssntl/radius-concentric.css";