# Craft Details Guide
Implementation details that separate polished products from rough ones. Most AI models miss these.
---
## 1. Focus States
Focus states are for keyboard navigation. Get them wrong and your app feels broken.
### The Rule: `:focus-visible`, Not `:focus`
```css
/* ❌ Shows focus ring on mouse click — annoying */
.button:focus {
outline: 2px solid var(--primary);
}
/* ✅ Shows focus ring only on keyboard navigation */
.button:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
```
**Why:** `:focus` triggers on any focus (including mouse click). `:focus-visible` only triggers when user is navigating with keyboard.
### Never Remove Focus Without Replacement
```css
/* ❌ NEVER — breaks keyboard navigation */
.button:focus {
outline: none;
}
/* ✅ Replace default with custom visible focus */
.button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--primary);
}
```
### Compound Controls: `:focus-within`
For groups where focus on any child should highlight the parent:
```css
/* Search input with icon */
.search-wrapper:focus-within {
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-tint);
}
```
---
## 2. Forms
Forms are where users struggle most. These details reduce friction.
### Input Types and Attributes
```html
```
### Autocomplete Matters
| Field | `autocomplete` value |
|-------|---------------------|
| Email | `email` |
| Password (login) | `current-password` |
| Password (signup) | `new-password` |
| Name | `name` |
| Phone | `tel` |
| Address | `street-address` |
| Credit card | `cc-number`, `cc-exp`, `cc-csc` |
| Non-auth fields | `off` (prevents password manager triggers) |
### Never Block Paste
```jsx
/* ❌ NEVER — accessibility violation, user hostile */
e.preventDefault()} />
/* ✅ Let users paste */
```
### Disable Spellcheck Where Appropriate
```html
```
### Labels and Hit Targets
```html
Email
Email
Accept terms
```
```css
/* No dead zones between checkbox and label */
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
```
### Placeholder Formatting
```html
```
### Submit Button States
```jsx
/* ✅ Button enabled until request starts, then shows spinner */
{isSubmitting ? : 'Save Changes'}
```
### Error Handling
```jsx
/* ✅ Errors inline, focus first error on submit */
```
### Unsaved Changes Warning
```js
// Warn before leaving with unsaved changes
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasUnsavedChanges]);
```
---
## 3. Images
Images are the #1 cause of layout shift (CLS). Fix them.
### Always Set Dimensions
```html
```
### Loading Strategy
```html
```
### In React/Next.js
```jsx
// Critical hero image
// Below fold
```
---
## 4. Touch & Mobile
Details that make mobile feel native.
### Tap Delay Removal
```css
/* Remove 300ms tap delay on mobile */
* {
touch-action: manipulation;
}
```
### Tap Highlight
```css
/* Set intentionally, don't just disable */
button, a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
```
### Modal Scroll Lock
```css
/* Prevent scroll chaining in modals/drawers */
.modal, .drawer, .sheet {
overscroll-behavior: contain;
}
```
### AutoFocus Rules
- Desktop only — avoid on mobile (opens keyboard unexpectedly)
- Single primary input per page maximum
- Must be justified — not "just because"
```jsx
/* ✅ Desktop-only autofocus */
```
---
## 5. Performance Patterns
Patterns that prevent jank.
### Virtualize Large Lists
Lists with 50+ items should be virtualized:
```jsx
// Use virtua, react-window, or similar
import { VList } from 'virtua';
{items.map(item =>
)}
```
**Or CSS-only for simpler cases:**
```css
.long-list {
content-visibility: auto;
contain-intrinsic-size: 0 50px; /* estimated item height */
}
```
### Avoid Layout Reads in Render
```jsx
/* ❌ Forces synchronous layout recalculation */
function Component() {
const height = elementRef.current.offsetHeight; // BAD
return
;
}
/* ✅ Use ResizeObserver or CSS */
function Component() {
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
const observer = new ResizeObserver(([entry]) => {
setHeight(entry.contentRect.height);
});
observer.observe(elementRef.current);
return () => observer.disconnect();
}, []);
return
;
}
```
### Uncontrolled Inputs When Possible
```jsx
/* ❌ Re-renders on every keystroke */
const [value, setValue] = useState('');
setValue(e.target.value)} />
/* ✅ No re-renders during typing */
/* Get value on submit */
const handleSubmit = () => {
const value = inputRef.current.value;
};
```
### Preconnect to CDNs
```html
```
---
## 6. Accessibility Quick Wins
High-impact, low-effort accessibility fixes.
### Semantic Elements
```html
Click me
Click me
Go here
Go here
```
### Keyboard Handlers
Interactive custom elements need keyboard support:
```jsx
{
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Custom button
```
### Heading Hierarchy
```html
Page Title
Section
Subsection
Another Section
Page Title
Section
```
### Scroll Margin for Anchors
```css
/* Prevents fixed header from covering anchor targets */
[id] {
scroll-margin-top: 80px; /* height of fixed header + buffer */
}
```
### Async Updates Need Announcements
```jsx
/* Toast notifications, validation messages */
{message}
```
---
## 7. Navigation & State
URL should reflect app state. Users expect to bookmark, share, and use back button.
### URL Reflects State
```jsx
/* ✅ Filters, tabs, pagination in URL */
// /products?category=shoes&sort=price&page=2
/* Use nuqs or similar for easy URL state sync */
import { useQueryState } from 'nuqs';
const [category, setCategory] = useQueryState('category');
```
### Links Support Browser Features
```jsx
/* ❌ onClick navigation — breaks Cmd+click, middle-click */
navigate('/page')}>Go
/* ✅ Proper link — all browser features work */
Go
```
### Destructive Actions Need Confirmation
```jsx
/* ❌ Immediate destructive action */
Delete Account
/* ✅ With confirmation */
setShowConfirmModal(true)}>Delete Account
/* Or with undo window */
Delete Account
// Toast: "Account deleted. Undo (10s)"
```
---
## 8. Content Copy Rules
Writing that converts.
| Rule | Example |
|------|---------|
| **Active voice** | "Install the CLI" not "The CLI will be installed" |
| **Title Case for headings/buttons** | "Save Changes" not "Save changes" |
| **Numerals for counts** | "8 deployments" not "eight deployments" |
| **Specific labels** | "Save API Key" not "Continue" |
| **Error messages include fix** | "Email invalid. Use format: name@domain.com" |
| **Second person** | "Your account" not "My account" |
| **& over "and"** (space-constrained) | "Terms & Privacy" |
---
## 9. Anti-Patterns Checklist
Flag these during code review:
- [ ] `user-scalable=no` or `maximum-scale=1` — disables zoom, accessibility violation
- [ ] `onPaste` with `preventDefault` — blocks paste, user hostile
- [ ] `transition: all` — performance killer, unpredictable
- [ ] `outline: none` without `:focus-visible` replacement — breaks keyboard nav
- [ ] `` for navigation — use `
` or ` `
- [ ] `` or `
` as buttons — use ``
- [ ] Images without `width`/`height` — causes CLS
- [ ] Large arrays `.map()` without virtualization (50+ items)
- [ ] Form inputs without labels — accessibility fail
- [ ] Icon buttons without `aria-label`
- [ ] Hardcoded date/number formats — use `Intl.DateTimeFormat`, `Intl.NumberFormat`
- [ ] `autoFocus` without clear justification
---
## Pre-Ship Checklist
- [ ] **Focus:** `:focus-visible` not `:focus`, never bare `outline: none`
- [ ] **Forms:** Correct `type`, `inputmode`, `autocomplete` attributes
- [ ] **Forms:** Labels on all inputs, no paste blocking
- [ ] **Images:** `width`/`height` set, `loading="lazy"` below fold
- [ ] **Touch:** `touch-action: manipulation`, `overscroll-behavior: contain` in modals
- [ ] **Performance:** Large lists virtualized, no layout reads in render
- [ ] **A11y:** Semantic HTML, keyboard handlers, heading hierarchy
- [ ] **URLs:** State reflected in URL, proper ``/` ` for navigation
- [ ] **Copy:** Active voice, specific labels, errors include fix
---
*Details matter. These patterns are the difference between "works" and "feels right."*