B

Your target audience

We're going to start with the hardest thing to do right, which is your ideal client and how they want to be spoken to.

Small, deliberate choices separate websites that work from websites that feel crafted. None of these tips will make or break your site on their own — but stacked together, they create that elusive sense of quality users can feel but rarely articulate.

1. Balance your headings

When a heading wraps to two or more lines, the default behavior often leaves a single orphaned word dangling on the last line. text-wrap: balance distributes the text evenly across lines, making headings look intentional rather than accidental.

For body text, use text-wrap: pretty instead — it focuses on eliminating widows in the last few lines.

h1, h2, h3 {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

A two-line heading that used to split unevenly now distributes words equally across lines. Much better.

text-wrap: balance

The complete guide to building interfaces

The complete guide to building interfaces

Change

2. Make your text crispy

On macOS, browsers default to subpixel antialiasing, which makes text — especially light text on dark backgrounds — appear heavier than the designer intended. A single declaration fixes this across your entire site.

body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

The difference is subtle in isolation but cumulative across the page. Your font-weight: 400 will actually look like 400, not a bloated 500.

-webkit-font-smoothing: antialiased

The quick brown fox jumps over the lazy dog. Typography matters in every detail.

The quick brown fox jumps over the lazy dog. Typography matters in every detail.

Change

3. Give numbers a fixed width

Counters, prices, timers, table columns — anywhere numbers update dynamically, the digits should be monospaced. Without this, a value going from 111,111 to 999,999 causes the entire layout to jitter because each digit has a different natural width.

.price, .counter, .table-cell {
  font-variant-numeric: tabular-nums;
}

This is especially visible in data-heavy dashboards, pricing cards, and any countdown component.

font-variant-numeric: tabular-nums

1,234.56

999,999.99

10.00

1,234.56

999,999.99

10.00

Change

4. Use layered shadows, not a single drop

A single box-shadow with a large blur looks flat and synthetic. Real-world light creates soft, layered shadows. Stack multiple shadows with increasing offsets and blur radii.

.card {
  box-shadow:
    0 1px 1px hsl(0 0% 0% / 0.06),
    0 2px 2px hsl(0 0% 0% / 0.06),
    0 4px 4px hsl(0 0% 0% / 0.06),
    0 8px 8px hsl(0 0% 0% / 0.06),
    0 16px 16px hsl(0 0% 0% / 0.06);
}

Each layer doubles the previous offset and blur. The result is a shadow that feels physically grounded rather than painted on.

Layered box-shadow

Card Title

Single flat shadow

Card Title

Five stacked shadows

Change

5. Match your shadow hue to your palette

Pure black shadows (rgba(0,0,0,...)) look dirty on colored backgrounds. A shadow tinted to match your page's dominant hue feels dramatically more natural.

.card {
  box-shadow:
    0 2px 4px hsl(220 60% 50% / 0.12),
    0 4px 8px hsl(220 60% 50% / 0.12);
}

On a warm, amber-toned site, shift the hue to 30-40. The shadows will stop looking like they belong to a different design system.

Hue-matched shadows

Card Title

Black shadow on blue background

Card Title

Blue-tinted shadow matching the hue

Change

6. Outline your images

Images with light edges bleed into light backgrounds, making the boundary invisible. A 1px outline at low opacity creates a subtle frame that works universally — on light backgrounds, dark backgrounds, and everything in between.

img {
  outline: 1px solid rgba(0, 0, 0, 0.1);
  outline-offset: -1px;
}

The outline-offset: -1px pulls the outline inside the image so it doesn't add to the element's visual size. It's the same technique Apple uses on product imagery across their site.

outline: 1px solid rgba(0,0,0,0.1)

Image
Image
Change

7. Use concentric border radii

When you nest rounded elements — a card inside a card, a button inside a rounded container — the outer radius must be larger than the inner one. The formula is: outer radius = inner radius + padding between them.

.outer {
  border-radius: 20px;  /* 12 + 8 */
  padding: 8px;
}

.inner {
  border-radius: 12px;
}

Without this, nested corners look pinched and uneven. With it, they look like they were designed as a single cohesive shape. This is one of those details that nobody consciously notices, but everyone feels.

Concentric border-radius

Inner
Inner
Change

8. Buttons need optical padding, not equal padding

A button with perfectly equal padding on all sides looks wrong. The icon side always appears to have more space than the text side because icons have built-in optical whitespace in their bounding box.

.button-with-leading-icon {
  padding: 10px 16px 10px 12px;
}

.button-with-trailing-icon {
  padding: 10px 12px 10px 16px;
}

Similarly, a play button inside a circle looks off-center even when it's mathematically centered. Nudge it 1-2px to the right to compensate for the triangle's visual weight.

Optical padding

Play
Play
Change

9. Use accent-color for instant form polish

Unstyled checkboxes, radio buttons, and range sliders are one of the most common tells of an unfinished site. You don't need to rebuild them from scratch — a single CSS property recolors all native form controls to match your brand.

:root {
  accent-color: #6c5ce7;
}

This gives you branded form elements that keep full accessibility, keyboard support, and native behavior — zero JavaScript, zero custom components.

accent-color

Option A
Option B
Option C
Option A
Option B
Option C
Change

10. Focus rings only when they matter

:focus styles show up on every click, which clutters a clean design. :focus-visible only activates when the user is navigating with a keyboard — exactly when they need the visual indicator.

button:focus {
  outline: none;
}

button:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}

Accessibility preserved, visual noise eliminated. This is one of the highest-leverage CSS changes you can make on any existing site.

:focus-visible

Button

Focus ring always visible (noisy)

Button

Focus ring only on keyboard nav

Change

11. Give clicks a physical response

A button that does nothing visually when pressed feels dead. A subtle scale transform on :active creates a satisfying tactile response — like pressing a real button.

.button {
  transition: transform 150ms ease-out;
}

.button:hover {
  transform: scale(1.02);
}

.button:active {
  transform: scale(0.97);
}

Keep the values tiny. You're simulating a physical press, not a cartoon bounce. The ease-out curve makes the movement fast at the start and gentle at the end — mimicking how real objects respond to force.

transform: scale() on :active

Click me

No feedback

Click me

Scale-down on press

Change

transform: scale() on :active

Click me

No visual feedback on press

Click me

Subtle scale-down on press (try clicking!)

Change

transform: scale() on :active

Click me

No feedback on press

Click me

Scale-down on press

Change

12. Fluid sizing without breakpoints

Media queries create hard jumps between sizes. clamp() creates smooth, continuous scaling. Use it for font sizes, container widths, and spacing.

h1 {
  font-size: clamp(1.75rem, 4vw, 3rem);
}

.container {
  padding-inline: clamp(1rem, 5vw, 3rem);
  max-width: clamp(320px, 90vw, 1200px);
}

A heading that smoothly scales from 28px on a phone to 48px on a desktop, without a single breakpoint, feels inherently more considered than one that snaps between two rigid sizes.

clamp() for fluid sizing

Fixed at 32px

Same size on every screen

Fluid with clamp()

Scales smoothly between 24px and 48px

Change

clamp() for fluid sizing

Fixed 32px

Same on every screen

Fluid clamp()

Scales 24px to 48px

Change

clamp() for fluid sizing

Fixed 32px

Same on every screen

Fluid clamp()

Scales 24px–48px

Change

13. Smooth scroll — but respect the user

Smooth scrolling makes anchor links and scroll-to-top buttons feel polished rather than teleporting. But some users have vestibular disorders that make animated movement nauseating. Wrap it in a preference check.

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

This is the correct pattern for all animations: start with no motion as the default, add it only for users who haven't opted out.

scroll-behavior: smooth

JUMP

Instant teleport to anchor

GLIDE

Smooth animated scroll to anchor

Change

scroll-behavior: smooth

JUMP

Instant teleport to anchor

GLIDE

Smooth scroll to anchor

Change

scroll-behavior: smooth

JUMP

Instant teleport

GLIDE

Smooth scroll

Change

scroll-behavior: smooth

JUMP

Instant teleport

GLIDE

Smooth scroll

Change

14. Snap your carousels

Native scroll snapping gives you buttery-smooth, physics-based carousel behavior without a single line of JavaScript. On mobile, it feels indistinguishable from a native app component.

.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 1rem;
  scrollbar-width: none;
}

.carousel > * {
  scroll-snap-align: center;
  flex-shrink: 0;
}

No libraries. No IntersectionObservers. No translateX calculations. Just CSS that works.

scroll-snap-type: x mandatory

1
2
3

Free scroll — stops anywhere

1
2
3

Snaps to each card center

Change

scroll-snap-type: x mandatory

1
2
3

Free scroll — stops anywhere

1
2
3

Snaps to each card

Change

scroll-snap-type

1
2
3

Stops anywhere

1
2
3

Snaps to cards

Change

15. Reserve space before images load

When images load without a defined aspect ratio, the content below them jumps — sometimes violently. This is called Cumulative Layout Shift (CLS), and it makes a site feel unreliable.

img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
  width: 100%;
  height: auto;
}

The browser now reserves the correct vertical space before the image arrives. The rest of the page stays perfectly still. This single change can dramatically improve your Core Web Vitals score.

aspect-ratio: 16 / 9

No aspect-ratio set

Content jumps when image loads

Space reserved

Layout stays stable during load

Change

aspect-ratio: 16 / 9

No ratio set

Content jumps on load

Space reserved

Layout stays stable

Change

16. Transition timing that feels alive

Not all easings are equal. ease is the CSS default, but it's generic and forgettable. ease-out — fast start, gentle end — is best for elements entering the screen. ease-in — slow start, fast end — is best for elements leaving. ease-in-out works for elements that stay in place but change state.

And a universal truth: keep durations between 150ms and 300ms. Below 150ms, the human eye can't perceive the interpolation. Above 300ms, the interface feels sluggish.

.element {
  transition: all 200ms ease-out;
}

Easing functions

linear

Robotic, constant speed

ease-out

Fast start, gentle end

Change

17. Exit animations should whisper

Entrance animations can afford drama — they're introducing something new. Exit animations should be fast, short, and subtle. An element leaving the screen doesn't need your attention; it needs to get out of the way.

.toast-enter {
  animation: slideIn 300ms ease-out;
}

.toast-exit {
  animation: fadeOut 150ms ease-in;
}

@keyframes slideIn {
  from { transform: translateY(100%); opacity: 0; }
}

@keyframes fadeOut {
  to { opacity: 0; transform: translateY(-8px); }
}

Notice the exit moves only 8px — a whisper compared to the full-height entrance. This asymmetry feels natural because it mirrors how attention works: arrivals are events, departures are transitions.

Exit vs entrance animation

Enter

300ms, full slide

Bold entrance: translateY(100%)

Exit

150ms, 8px nudge

Subtle exit: translateY(-8px)

Change

18. Frosted glass that actually works

The glassmorphism trend is everywhere, but most implementations look muddy because they skip the crucial details: a subtle border and proper background opacity.

.glass {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
}

The border catches light at the edges, just like real frosted glass. Without it, the element looks like a rectangle with a Gaussian blur — which is technically what it is, but you don't want it to feel that way.

backdrop-filter + border

Glass

No border — muddy edges

Glass

1px border catches light

Change

19. Build a spacing system and stick to it

The fastest way to make a site look "designed" is consistent spacing. Define a scale based on a base unit (typically 4px or 8px), expose it as custom properties, and use nothing else.

:root {
  --space-1: 0.25rem;   /* 4px  */
  --space-2: 0.5rem;    /* 8px  */
  --space-3: 0.75rem;   /* 12px */
  --space-4: 1rem;      /* 16px */
  --space-6: 1.5rem;    /* 24px */
  --space-8: 2rem;      /* 32px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
}

When every margin, padding, and gap snaps to this scale, elements fall into natural visual rhythms. The page starts to feel like a grid even when it isn't.

Spacing system

Random spacing: 13px, 7px, 22px

System spacing: 16px, 16px, 16px

Change

20. Stagger your entrance animations

Animating a group of elements all at once looks mechanical. Adding a small sequential delay between them — 60 to 100ms — makes the entrance feel organic, like dominoes falling.

.card {
  animation: fadeUp 400ms ease-out both;
  animation-delay: calc(var(--i, 0) * 80ms);
}

The stagger makes each element feel like it has its own moment. It turns a static page into a choreographed experience.

Staggered animation

A
B
C

All appear at once

A
B
C

80ms stagger between each

Change

None of these details are revolutionary. You won't find them in a design system's changelog or a sprint retrospective. But they're the reason some websites feel right the moment you land on them — and others feel like they were assembled rather than designed.

Ship the details.

Get a site your sales team can actually use. In 6 weeks.