Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design engineering principles for polished UI details: concentric border radius, interruptible animations, shadows, and font smoothing.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
surfaces.md
1# Surfaces23Border radius, optical alignment, shadows, and image outlines.45## Concentric Border Radius67When nesting rounded elements, the outer radius must equal the inner radius plus the padding between them:89```10outerRadius = innerRadius + padding11```1213This rule is most useful when nested surfaces are close together. If padding is larger than `24px`, treat the layers as separate surfaces and choose each radius independently instead of forcing strict concentric math.1415### Example1617```css18/* Good — concentric radii */19.card {20border-radius: 20px; /* 12 + 8 */21padding: 8px;22}23.card-inner {24border-radius: 12px;25}2627/* Bad — same radius on both */28.card {29border-radius: 12px;30padding: 8px;31}32.card-inner {33border-radius: 12px;34}35```3637### Tailwind Example3839```tsx40// Good — outer radius accounts for padding41<div className="rounded-2xl p-2"> {/* 16px radius, 8px padding */}42<div className="rounded-lg"> {/* 8px radius = 16 - 8 ✓ */}43...44</div>45</div>4647// Bad — same radius on both48<div className="rounded-xl p-2">49<div className="rounded-xl"> {/* same radius, looks off */}50...51</div>52</div>53```5455Mismatched border radii on nested elements is one of the most common things that makes interfaces feel off. Always calculate concentrically.5657## Optical Alignment5859When geometric centering looks off, align optically instead.6061### Buttons with Text + Icon6263Use slightly less padding on the icon side to make the button feel balanced. A reliable rule of thumb is:64`icon-side padding = text-side padding - 2px`.6566```css67/* Good — less padding on icon side */68.button-with-icon {69padding-left: 16px;70padding-right: 14px; /* icon side = text side - 2px */71}7273/* Bad — equal padding looks like icon is pushed too far right */74.button-with-icon {75padding: 0 16px;76}77```7879```tsx80// Tailwind81<button className="pl-4 pr-3.5 flex items-center gap-2">82<span>Continue</span>83<ArrowRightIcon />84</button>85```8687### Play Button Triangles8889Play icons are triangular and their geometric center is not their visual center. Shift slightly right:9091```css92/* Good — optically centered */93.play-button svg {94margin-left: 2px; /* shift right to account for triangle shape */95}9697/* Bad — geometrically centered but looks off */98.play-button svg {99/* no adjustment */100}101```102103### Asymmetric Icons (Stars, Arrows, Carets)104105Some icons have uneven visual weight. The best fix is adjusting the SVG directly so no extra margin/padding is needed in the component code.106107```tsx108// Best — fix in the SVG itself109// Adjust the viewBox or path to visually center the icon110111// Fallback — adjust with margin112<span className="ml-px">113<StarIcon />114</span>115```116117## Shadows Instead of Borders118119For **buttons, cards, and containers** that use a border for depth or elevation, prefer replacing it with a subtle `box-shadow`. Shadows adapt to any background since they use transparency; solid borders don't. This also helps when using images or multiple colors as backgrounds — solid border colors don't work well on backgrounds other than the ones they were designed for.120121**Do not apply this to dividers** (`border-b`, `border-t`, side borders) or any border whose purpose is layout separation rather than element depth. Those should stay as borders.122123### Shadow as Border (Light Mode)124125The shadow is comprised of three layers. The first acts as a 1px border ring, the second adds subtle lift, and the third provides ambient depth:126127```css128:root {129--shadow-border:1300px 0px 0px 1px rgba(0, 0, 0, 0.06),1310px 1px 2px -1px rgba(0, 0, 0, 0.06),1320px 2px 4px 0px rgba(0, 0, 0, 0.04);133--shadow-border-hover:1340px 0px 0px 1px rgba(0, 0, 0, 0.08),1350px 1px 2px -1px rgba(0, 0, 0, 0.08),1360px 2px 4px 0px rgba(0, 0, 0, 0.06);137}138```139140### Shadow as Border (Dark Mode)141142In dark mode, simplify to a single white ring — layered depth shadows aren't visible on dark backgrounds:143144```css145/* Dark mode — adapt to whatever setup the project uses146(prefers-color-scheme, class, data attribute, etc.) */147--shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08);148--shadow-border-hover: 0 0 0 1px rgba(255, 255, 255, 0.13);149```150151### Usage with Hover Transition152153Apply the variable and add `transition-[box-shadow]` for a smooth hover:154155```css156.card {157box-shadow: var(--shadow-border);158transition-property: box-shadow;159transition-duration: 150ms;160transition-timing-function: ease-out;161}162163.card:hover {164box-shadow: var(--shadow-border-hover);165}166```167168### When to Use Shadows vs. Borders169170| Use shadows | Use borders |171| --- | --- |172| Cards, containers with depth | Dividers between list items |173| Buttons with bordered styles | Table cell boundaries |174| Elevated elements (dropdowns, modals) | Form input outlines (for accessibility) |175| Elements on varied backgrounds | Hairline separators in dense UI |176| Hover/focus states for lift effect | |177178## Image Outlines179180Add a subtle `1px` outline with low opacity to images. This creates consistent depth, especially in design systems where other elements use borders or shadows.181182### Color rules (non-negotiable)183184- **Light mode**: pure black — `rgba(0, 0, 0, 0.1)`. Exact values: R=0, G=0, B=0.185- **Dark mode**: pure white — `rgba(255, 255, 255, 0.1)`. Exact values: R=255, G=255, B=255.186- Never use a near-black or near-white from the project palette (e.g. slate-900, zinc-900, `#0a0a0a`, `#111827`, `#f5f5f7`). Tinted outlines pick up the surrounding surface color and read as dirt on the image edge.187- Never match the outline to the project's accent or ink color. The outline is a neutral separator, not a themed element.188189### Light Mode190191```css192img {193outline: 1px solid rgba(0, 0, 0, 0.1);194outline-offset: -1px; /* inset so it doesn't add to layout */195}196```197198### Dark Mode199200```css201img {202outline: 1px solid rgba(255, 255, 255, 0.1);203outline-offset: -1px;204}205```206207### Tailwind with Dark Mode208209```tsx210<img211className="outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/10"212src={src}213alt={alt}214/>215```216217Use `outline-black/10` and `outline-white/10` specifically — not `outline-slate-*`, `outline-zinc-*`, `outline-neutral-*`, or any tinted scale.218219**Why outline instead of border?** `outline` doesn't affect layout (no added width/height), and `outline-offset: -1px` keeps it inset so images stay their intended size.220221## Minimum Hit Area222223Interactive elements should have a minimum hit area of 44×44px (WCAG) or at least 40×40px. If the visible element is smaller (e.g., a 20×20 checkbox), extend the hit area with a pseudo-element.224225### CSS Example226227```css228/* Small checkbox with expanded hit area */229.checkbox {230position: relative;231width: 20px;232height: 20px;233}234235.checkbox::after {236content: "";237position: absolute;238top: 50%;239left: 50%;240transform: translate(-50%, -50%);241width: 40px;242height: 40px;243}244```245246### Tailwind Example247248```tsx249<button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-10 after:-translate-1/2">250<CheckIcon />251</button>252```253254### Collision Rule255256If the extended hit area overlaps another interactive element, shrink the pseudo-element — but make it as large as possible without colliding. Two interactive elements should never have overlapping hit areas.257