Product design skills

surfaces.md

A supporting file of the better-ui skill.

Surfaces

Border radius, optical alignment, shadows and image outlines.

Concentric border radius

When nesting rounded elements, the outer radius must equal the inner radius plus the padding between them:

outerRadius = innerRadius + padding

The rule matters most when nested surfaces sit close together. Past 24px of padding, treat the layers as separate surfaces and choose each radius independently rather than forcing concentric math.

Example

/* Good: concentric radii */
.card {
  border-radius: 20px; /* 12 + 8 */
  padding: 8px;
}
.card-inner {
  border-radius: 12px;
}

/* Bad: same radius on both */
.card {
  border-radius: 12px;
  padding: 8px;
}
.card-inner {
  border-radius: 12px;
}

Tailwind example

// Good: outer radius accounts for padding
<div className="rounded-2xl p-2">       {/* 16px radius, 8px padding */}
  <div className="rounded-lg">          {/* 8px radius = 16 - 8 ✓ */}
    ...
  </div>
</div>

// Bad: same radius on both
<div className="rounded-xl p-2">
  <div className="rounded-xl">          {/* same radius, looks off */}
    ...
  </div>
</div>

Mismatched radii on closely nested surfaces are a common source of visual tension. Calculate concentrically where the layers share a visible, even inset. Keep an established component token where they are independent or the padding is deliberately asymmetric.

Optical alignment

When geometric centering looks off, align optically instead.

Buttons with text + icon

Where an icon makes symmetric padding look unbalanced, use slightly less on the icon side. A starting point: icon-side padding = text-side padding - 2px.

/* Good: less padding on icon side */
.button-with-icon {
  padding-inline-start: 16px;
  padding-inline-end: 14px; /* trailing icon side = text side - 2px */
}

/* Bad: equal padding looks like icon is pushed too far right */
.button-with-icon {
  padding-inline: 16px;
}
// Tailwind
<button className="ps-4 pe-3.5 flex items-center gap-2">
  <span>Continue</span>
  <ArrowRightIcon />
</button>

Play button triangles

Play icons are triangular and their geometric center is not their visual center. Shift slightly right:

/* Good: optically centered */
.play-button svg {
  transform: translateX(2px); /* physical correction to the glyph itself */
}

/* Bad: geometrically centered but looks off */
.play-button svg {
  /* no adjustment */
}

Asymmetric icons (stars, arrows, carets)

Some icons carry uneven visual weight. The best fix is adjusting the SVG directly, so the component needs no extra margin or padding.

// Best: fix in the SVG itself
// Adjust the viewBox or path to visually center the icon

// Fallback: adjust with margin
<span className="translate-x-px">
  <StarIcon />
</span>

Shadows instead of borders

Where buttons, cards and containers use a border for depth or elevation, replace it with a subtle box-shadow. Shadows use transparency, so they adapt to any background where solid borders do not. That matters most over images or multiple background colors, which a fixed border color was never designed for.

Never apply this to dividers, meaning border-b, border-t and side borders, or any border whose purpose is layout separation rather than depth. Those stay borders.

Shadow as border (light mode)

Three layers. The first acts as a 1px border ring, the second adds subtle lift, the third ambient depth:

:root {
  --shadow-border:
    0px 0px 0px 1px oklch(0 0 0 / 0.06),
    0px 1px 2px -1px oklch(0 0 0 / 0.06),
    0px 2px 4px 0px oklch(0 0 0 / 0.04);
  --shadow-border-hover:
    0px 0px 0px 1px oklch(0 0 0 / 0.08),
    0px 1px 2px -1px oklch(0 0 0 / 0.08),
    0px 2px 4px 0px oklch(0 0 0 / 0.06);
}

Shadow as border (dark mode)

In dark mode, simplify to one white ring, since layered depth shadows are invisible on dark backgrounds:

/* Dark mode: adapt to whatever setup the project uses
   (prefers-color-scheme, class, data attribute, etc.) */
--shadow-border: 0 0 0 1px oklch(1 0 0 / 0.08);
--shadow-border-hover: 0 0 0 1px oklch(1 0 0 / 0.13);

Usage with hover transition

Apply the variable and add transition-[box-shadow] for a smooth hover:

.card {
  box-shadow: var(--shadow-border);
  transition-property: box-shadow;
  transition-duration: 150ms;
  transition-timing-function: ease-out;
}

.card:hover {
  box-shadow: var(--shadow-border-hover);
}

When to use shadows vs. borders

Use shadowsUse borders
Cards, containers with depthDividers between list items
Buttons with bordered stylesTable cell boundaries
Elevated elements (dropdowns, modals)Form input outlines (for accessibility)
Elements on varied backgroundsHairline separators in dense UI
Hover/focus states for lift effect

Image outlines

Add a 1px outline at low opacity to images for consistent depth, especially where other elements use borders or shadows.

Color rules (non-negotiable)

  • Light mode: pure black, oklch(0 0 0 / 0.1).
  • Dark mode: pure white, oklch(1 0 0 / 0.1).
  • Never a near-black or near-white from the project palette, such as slate-900, zinc-900, #0a0a0a, #111827, or #f5f5f7. Tinted outlines pick up the surrounding surface color and read as dirt on the image edge.
  • Never match the outline to the project's accent or ink color. The outline is a neutral separator, not a themed element.

Light mode

img {
  outline: 1px solid oklch(0 0 0 / 0.1);
  outline-offset: -1px; /* draw the ring just inside the image edge */
}

Dark mode

img {
  outline: 1px solid oklch(1 0 0 / 0.1);
  outline-offset: -1px;
}

Tailwind with dark mode

<img
  className="outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10"
  src={src}
  alt={alt}
/>

Use outline-black/10 and outline-white/10 specifically, not outline-slate-*, outline-zinc-*, outline-neutral-*, or any tinted scale.

Why outline instead of border? outline never affects layout, adding no width or height at any offset, and outline-offset: -1px draws the ring just inside the image edge so it hugs the corner radius.

On this page