@ctxr/skill-frontend-excellence 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/LICENSE +21 -0
- package/README.md +114 -0
- package/SKILL.md +227 -0
- package/package.json +63 -0
- package/references/accessibility.md +396 -0
- package/references/audit-workflow.md +390 -0
- package/references/components.md +247 -0
- package/references/data-viz.md +457 -0
- package/references/defects.md +152 -0
- package/references/design.md +513 -0
- package/references/forms.md +485 -0
- package/references/lighthouse.md +242 -0
- package/references/motion.md +642 -0
- package/references/performance.md +416 -0
- package/references/pre-launch.md +342 -0
- package/references/responsive.md +519 -0
- package/references/seo.md +422 -0
- package/references/ui-ux.md +565 -0
- package/scripts/check-no-dashes.mjs +90 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# Responsive Layout
|
|
2
|
+
|
|
3
|
+
Framework-agnostic guidance on building layouts that work across the full range of devices, from 320px phones to 4K displays, in portrait and landscape, with light and dark themes, with system text scaling, and with browser zoom.
|
|
4
|
+
|
|
5
|
+
## Mobile-First
|
|
6
|
+
|
|
7
|
+
Always design and code mobile first.
|
|
8
|
+
|
|
9
|
+
- Mobile constraints (small viewport, touch input, slow network, limited CPU) force the right tradeoffs.
|
|
10
|
+
- Adding desktop affordances on top of mobile is easier than stripping mobile complexity from a desktop-first design.
|
|
11
|
+
- Most users are mobile. Treating mobile as an afterthought is treating users as an afterthought.
|
|
12
|
+
|
|
13
|
+
CSS pattern:
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
/* Default styles target mobile */
|
|
17
|
+
.nav {
|
|
18
|
+
padding: 12px;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Min-width media queries layer up */
|
|
23
|
+
@media (min-width: 768px) {
|
|
24
|
+
.nav {
|
|
25
|
+
padding: 16px 24px;
|
|
26
|
+
flex-direction: row;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Avoid `max-width` queries layered downward; they invert the natural flow.
|
|
32
|
+
|
|
33
|
+
## Breakpoints
|
|
34
|
+
|
|
35
|
+
Pick a small, consistent set. Common scales:
|
|
36
|
+
|
|
37
|
+
| Breakpoint | Pixel | Targets |
|
|
38
|
+
|-----------|-------|---------|
|
|
39
|
+
| (default) | < 640px | Phones |
|
|
40
|
+
| `sm` | 640px | Large phones, small tablets |
|
|
41
|
+
| `md` | 768px | Tablets portrait |
|
|
42
|
+
| `lg` | 1024px | Tablets landscape, small laptops |
|
|
43
|
+
| `xl` | 1280px | Laptops, desktops |
|
|
44
|
+
| `2xl` | 1536px | Large desktops |
|
|
45
|
+
| `3xl` (optional) | 1920px | Very large displays |
|
|
46
|
+
|
|
47
|
+
Tailwind's defaults match this. Custom breakpoints should be deliberate.
|
|
48
|
+
|
|
49
|
+
Don't add a breakpoint per device. Three or four well-chosen breakpoints handle every reasonable layout.
|
|
50
|
+
|
|
51
|
+
## Canonical Audit Capture Viewports
|
|
52
|
+
|
|
53
|
+
For multi-page polish work, two specific viewport sizes are the canonical capture targets. They are the sizes used by the screenshot loop and the geometry sweep.
|
|
54
|
+
|
|
55
|
+
| Role | Width x Height | Notes |
|
|
56
|
+
|------|----------------|-------|
|
|
57
|
+
| Desktop audit | `1440x900` | Common laptop and external display proxy; wide enough to see desktop nav, narrow enough to catch hero overflow |
|
|
58
|
+
| Mobile audit | `375x812` | iPhone-class viewport; small enough to expose mobile drawer, drift, and touch-target failures |
|
|
59
|
+
|
|
60
|
+
Both sizes are required for any cross-page audit. See [audit-workflow.md](audit-workflow.md) Phase 4 for the capture procedure and [defects.md](defects.md) for the geometry sweep that runs at both sizes.
|
|
61
|
+
|
|
62
|
+
## Container Queries
|
|
63
|
+
|
|
64
|
+
Container queries (`@container`) let components respond to their container, not the viewport. Use when the same component appears in different layouts:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
.card {
|
|
68
|
+
container-type: inline-size;
|
|
69
|
+
container-name: card;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@container card (min-width: 400px) {
|
|
73
|
+
.card-body {
|
|
74
|
+
flex-direction: row;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When to use:
|
|
80
|
+
|
|
81
|
+
- A card that's full-width on mobile and a 1/3 column on desktop, where its internal layout should respond to its width, not the viewport.
|
|
82
|
+
- Reusable components that appear in sidebars, modals, and main content.
|
|
83
|
+
|
|
84
|
+
When NOT to use:
|
|
85
|
+
|
|
86
|
+
- Top-level page layout. Viewport queries are simpler and more universally supported.
|
|
87
|
+
|
|
88
|
+
## Fluid Typography
|
|
89
|
+
|
|
90
|
+
Use `clamp()` to scale type smoothly between breakpoints rather than stepping at fixed media queries:
|
|
91
|
+
|
|
92
|
+
```css
|
|
93
|
+
.h1 {
|
|
94
|
+
font-size: clamp(2rem, 1.5rem + 2.5vw, 4rem);
|
|
95
|
+
/* min 32px, max 64px, scales with viewport between */
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Format: `clamp(min, preferred, max)`. The preferred value should reach the min at the smallest viewport you support and the max at the largest.
|
|
100
|
+
|
|
101
|
+
For vertical rhythm, also use clamp on `line-height` if needed, though usually keeping `line-height` as a unitless multiplier (`line-height: 1.2`) handles fluid scaling automatically.
|
|
102
|
+
|
|
103
|
+
## Viewport Units
|
|
104
|
+
|
|
105
|
+
| Unit | Meaning |
|
|
106
|
+
|------|--------|
|
|
107
|
+
| `vw` / `vh` | 1% of viewport width/height (legacy, includes browser chrome on mobile) |
|
|
108
|
+
| `dvw` / `dvh` | Dynamic viewport (changes as browser chrome shows/hides). Use for full-height mobile layouts. |
|
|
109
|
+
| `svw` / `svh` | Small viewport (smallest possible, browser chrome visible) |
|
|
110
|
+
| `lvw` / `lvh` | Large viewport (largest possible, browser chrome hidden) |
|
|
111
|
+
| `cqw` / `cqh` | Container query units |
|
|
112
|
+
|
|
113
|
+
For full-height mobile heroes, use `min-height: 100dvh` (not `100vh`). `100vh` overflows on iOS Safari when the address bar is showing.
|
|
114
|
+
|
|
115
|
+
## Safe Areas
|
|
116
|
+
|
|
117
|
+
Modern phones have notches, dynamic islands, gesture bars, and home indicators. Respect them.
|
|
118
|
+
|
|
119
|
+
```css
|
|
120
|
+
.fixed-bottom-bar {
|
|
121
|
+
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.fixed-top-bar {
|
|
125
|
+
padding-top: max(16px, env(safe-area-inset-top));
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
In your viewport meta:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The `viewport-fit=cover` is required for `env(safe-area-inset-*)` to work properly.
|
|
136
|
+
|
|
137
|
+
## Logical Properties
|
|
138
|
+
|
|
139
|
+
For RTL support, use logical properties instead of physical:
|
|
140
|
+
|
|
141
|
+
| Physical | Logical |
|
|
142
|
+
|---------|---------|
|
|
143
|
+
| `margin-left` | `margin-inline-start` |
|
|
144
|
+
| `margin-right` | `margin-inline-end` |
|
|
145
|
+
| `padding-top` | `padding-block-start` |
|
|
146
|
+
| `padding-bottom` | `padding-block-end` |
|
|
147
|
+
| `text-align: left` | `text-align: start` |
|
|
148
|
+
| `text-align: right` | `text-align: end` |
|
|
149
|
+
| `border-left` | `border-inline-start` |
|
|
150
|
+
| `width` | `inline-size` |
|
|
151
|
+
| `height` | `block-size` |
|
|
152
|
+
|
|
153
|
+
When `<html dir="rtl">`, logical properties automatically flip. Physical properties don't.
|
|
154
|
+
|
|
155
|
+
## Layout Primitives
|
|
156
|
+
|
|
157
|
+
Build layouts from a small set of primitives:
|
|
158
|
+
|
|
159
|
+
### Stack
|
|
160
|
+
|
|
161
|
+
Vertical layout with consistent gap.
|
|
162
|
+
|
|
163
|
+
```css
|
|
164
|
+
.stack {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
gap: var(--space-4);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Cluster
|
|
172
|
+
|
|
173
|
+
Horizontal layout, wraps to multiple lines if needed, consistent gap.
|
|
174
|
+
|
|
175
|
+
```css
|
|
176
|
+
.cluster {
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-wrap: wrap;
|
|
179
|
+
gap: var(--space-3);
|
|
180
|
+
align-items: center;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Sidebar
|
|
185
|
+
|
|
186
|
+
Two-column layout where one column is fixed-width and the other flexes.
|
|
187
|
+
|
|
188
|
+
```css
|
|
189
|
+
.with-sidebar {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
gap: var(--space-6);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.with-sidebar > .sidebar {
|
|
196
|
+
flex-basis: 280px;
|
|
197
|
+
flex-grow: 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.with-sidebar > .main {
|
|
201
|
+
flex-basis: 0;
|
|
202
|
+
flex-grow: 999;
|
|
203
|
+
min-inline-size: 50%;
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Switcher
|
|
208
|
+
|
|
209
|
+
Multi-column layout that becomes a stack below a threshold.
|
|
210
|
+
|
|
211
|
+
```css
|
|
212
|
+
.switcher {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
gap: var(--space-4);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.switcher > * {
|
|
219
|
+
flex-grow: 1;
|
|
220
|
+
flex-basis: calc((600px - 100%) * 999);
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
When the container drops below 600px, items stack.
|
|
225
|
+
|
|
226
|
+
### Center
|
|
227
|
+
|
|
228
|
+
Center an element with optional max-width.
|
|
229
|
+
|
|
230
|
+
```css
|
|
231
|
+
.center {
|
|
232
|
+
box-sizing: content-box;
|
|
233
|
+
max-inline-size: 65ch;
|
|
234
|
+
margin-inline: auto;
|
|
235
|
+
padding-inline: var(--space-4);
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Cover
|
|
240
|
+
|
|
241
|
+
Fill the available space with header, centered content, footer.
|
|
242
|
+
|
|
243
|
+
```css
|
|
244
|
+
.cover {
|
|
245
|
+
display: flex;
|
|
246
|
+
flex-direction: column;
|
|
247
|
+
min-block-size: 100dvh;
|
|
248
|
+
padding: var(--space-4);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.cover > main {
|
|
252
|
+
margin-block: auto;
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Frame
|
|
257
|
+
|
|
258
|
+
A box with a fixed aspect ratio.
|
|
259
|
+
|
|
260
|
+
```css
|
|
261
|
+
.frame {
|
|
262
|
+
aspect-ratio: 16 / 9;
|
|
263
|
+
overflow: hidden;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.frame > img,
|
|
267
|
+
.frame > video {
|
|
268
|
+
inline-size: 100%;
|
|
269
|
+
block-size: 100%;
|
|
270
|
+
object-fit: cover;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Reel
|
|
275
|
+
|
|
276
|
+
Horizontal scrolling cluster (carousel without arrows).
|
|
277
|
+
|
|
278
|
+
```css
|
|
279
|
+
.reel {
|
|
280
|
+
display: flex;
|
|
281
|
+
gap: var(--space-4);
|
|
282
|
+
overflow-x: auto;
|
|
283
|
+
scroll-snap-type: x mandatory;
|
|
284
|
+
scrollbar-width: thin;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.reel > * {
|
|
288
|
+
flex: 0 0 auto;
|
|
289
|
+
scroll-snap-align: start;
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Grid
|
|
294
|
+
|
|
295
|
+
CSS Grid with auto-fit:
|
|
296
|
+
|
|
297
|
+
```css
|
|
298
|
+
.grid {
|
|
299
|
+
display: grid;
|
|
300
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
301
|
+
gap: var(--space-4);
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
This wraps from 1 column to 2/3/4 columns as the container grows.
|
|
306
|
+
|
|
307
|
+
## Containers and Max Widths
|
|
308
|
+
|
|
309
|
+
### Page container
|
|
310
|
+
|
|
311
|
+
Most surfaces benefit from a max content width:
|
|
312
|
+
|
|
313
|
+
```css
|
|
314
|
+
.container {
|
|
315
|
+
max-inline-size: var(--container-width, 1280px);
|
|
316
|
+
margin-inline: auto;
|
|
317
|
+
padding-inline: clamp(16px, 4vw, 48px);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Common max-widths:
|
|
322
|
+
|
|
323
|
+
| Width | Use |
|
|
324
|
+
|-------|-----|
|
|
325
|
+
| 640px | Long-form reading content |
|
|
326
|
+
| 768px | Reading with a sidebar |
|
|
327
|
+
| 1024px | Routine application UI |
|
|
328
|
+
| 1280px | Hero or feature-rich layouts |
|
|
329
|
+
| 1440px | Big imagery, generous whitespace |
|
|
330
|
+
| 1600px | Wide screens, data-heavy surfaces |
|
|
331
|
+
|
|
332
|
+
Don't go full-width on huge displays. Lines longer than ~75 characters become hard to read.
|
|
333
|
+
|
|
334
|
+
### Reading containers
|
|
335
|
+
|
|
336
|
+
For prose, set `max-inline-size: 65ch` to constrain line length.
|
|
337
|
+
|
|
338
|
+
## Horizontal Scroll: Forbidden
|
|
339
|
+
|
|
340
|
+
The single most common responsive bug: horizontal scroll on mobile.
|
|
341
|
+
|
|
342
|
+
Causes:
|
|
343
|
+
|
|
344
|
+
- Fixed-pixel widths exceeding viewport (`width: 1200px` on a 375px screen).
|
|
345
|
+
- Long unbreakable text (URLs, code, words in CJK).
|
|
346
|
+
- Images without `max-width: 100%`.
|
|
347
|
+
- Tables without overflow handling.
|
|
348
|
+
- Sidebars that don't collapse.
|
|
349
|
+
|
|
350
|
+
Fixes:
|
|
351
|
+
|
|
352
|
+
- Use `max-inline-size: 100%` (or `max-width: 100%` for non-RTL contexts) on images and embeds.
|
|
353
|
+
- Use `overflow-wrap: break-word` and `word-break: break-word` for long strings.
|
|
354
|
+
- Use `<div style="overflow-x: auto">` to wrap wide tables.
|
|
355
|
+
- Make sidebars collapse at small breakpoints.
|
|
356
|
+
|
|
357
|
+
Test: at 320px width, scroll horizontally; if you can, fix it.
|
|
358
|
+
|
|
359
|
+
## Accessibility at Different Viewports
|
|
360
|
+
|
|
361
|
+
WCAG 1.4.10 (Reflow): content must be usable at 320 CSS pixels wide without horizontal scroll.
|
|
362
|
+
|
|
363
|
+
WCAG 1.4.4 (Resize Text): content must be usable at 200% browser zoom without loss of functionality.
|
|
364
|
+
|
|
365
|
+
Test:
|
|
366
|
+
|
|
367
|
+
- Resize the browser to 320px wide.
|
|
368
|
+
- Set browser zoom to 200%.
|
|
369
|
+
- Set OS text size to largest.
|
|
370
|
+
|
|
371
|
+
If the layout breaks, fix it.
|
|
372
|
+
|
|
373
|
+
## Orientation
|
|
374
|
+
|
|
375
|
+
Some users hold phones in landscape. Don't lock layout to portrait.
|
|
376
|
+
|
|
377
|
+
- Don't assume landscape phones have a tablet-class screen height (they're short).
|
|
378
|
+
- For full-screen forms or inputs, support both orientations.
|
|
379
|
+
- Test landscape mode on an actual phone.
|
|
380
|
+
|
|
381
|
+
## Dynamic Type / Font Scaling
|
|
382
|
+
|
|
383
|
+
iOS and Android both support system-wide font scaling for accessibility. Web should respect browser zoom (which scales `rem`) and the system text size where exposed.
|
|
384
|
+
|
|
385
|
+
```css
|
|
386
|
+
/* Use rem for font-size so user agent text scaling works */
|
|
387
|
+
body {
|
|
388
|
+
font-size: 1rem; /* defaults to 16px, scales with user prefs */
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
h1 {
|
|
392
|
+
font-size: 2.5rem; /* scales proportionally */
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Avoid `px` for font sizes. Use `rem` everywhere except where pixel-perfect rendering matters (logos, single-line UI labels).
|
|
397
|
+
|
|
398
|
+
## Reduced Motion
|
|
399
|
+
|
|
400
|
+
Respect `prefers-reduced-motion`. See [motion.md](motion.md).
|
|
401
|
+
|
|
402
|
+
## Reduced Data
|
|
403
|
+
|
|
404
|
+
```css
|
|
405
|
+
@media (prefers-reduced-data: reduce) {
|
|
406
|
+
/* Skip non-essential animations, replace videos with posters, lower image quality */
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Not yet widely supported, but adding the rule costs nothing.
|
|
411
|
+
|
|
412
|
+
## High Contrast
|
|
413
|
+
|
|
414
|
+
```css
|
|
415
|
+
@media (prefers-contrast: more) {
|
|
416
|
+
/* Bump border weights, increase contrast, simplify backgrounds */
|
|
417
|
+
:root {
|
|
418
|
+
--color-border: var(--color-foreground);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Color Scheme
|
|
424
|
+
|
|
425
|
+
```css
|
|
426
|
+
@media (prefers-color-scheme: dark) {
|
|
427
|
+
:root {
|
|
428
|
+
/* Dark mode tokens */
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Or explicitly with a `[data-theme="dark"]` selector for user-controlled toggle.
|
|
434
|
+
|
|
435
|
+
Use `color-scheme` to opt in to native UA dark mode controls (scrollbars, form controls):
|
|
436
|
+
|
|
437
|
+
```css
|
|
438
|
+
:root { color-scheme: light dark; }
|
|
439
|
+
[data-theme="light"] { color-scheme: light; }
|
|
440
|
+
[data-theme="dark"] { color-scheme: dark; }
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Print
|
|
444
|
+
|
|
445
|
+
Content-rich surfaces may be printed. Add a minimal print stylesheet:
|
|
446
|
+
|
|
447
|
+
```css
|
|
448
|
+
@media print {
|
|
449
|
+
nav, footer, .no-print { display: none; }
|
|
450
|
+
body { font-size: 12pt; color: black; background: white; }
|
|
451
|
+
a { color: black; text-decoration: underline; }
|
|
452
|
+
a[href]::after { content: " (" attr(href) ")"; }
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Touch and Pointer
|
|
457
|
+
|
|
458
|
+
Different input modalities have different needs:
|
|
459
|
+
|
|
460
|
+
```css
|
|
461
|
+
/* Hover-capable pointer */
|
|
462
|
+
@media (hover: hover) and (pointer: fine) {
|
|
463
|
+
.card:hover { transform: translateY(-2px); }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* Touch-only */
|
|
467
|
+
@media (hover: none) and (pointer: coarse) {
|
|
468
|
+
/* Larger hit targets, no hover-only affordances */
|
|
469
|
+
.icon-btn { padding: 12px; }
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Pointer types:
|
|
474
|
+
|
|
475
|
+
- `fine`: mouse, trackpad, stylus.
|
|
476
|
+
- `coarse`: touch.
|
|
477
|
+
- `hover` capability indicates the device can hover (hover-on-hold for touch is not real hover).
|
|
478
|
+
|
|
479
|
+
## Common Responsive Mistakes
|
|
480
|
+
|
|
481
|
+
- One desktop-first stylesheet with `max-width` overrides that pile up.
|
|
482
|
+
- Fixed-pixel widths (`width: 1200px`) on containers.
|
|
483
|
+
- Images without `max-width: 100%`.
|
|
484
|
+
- Tables without horizontal scroll.
|
|
485
|
+
- Sidebars that don't collapse.
|
|
486
|
+
- Hero text that's readable on desktop and a wall of words on mobile.
|
|
487
|
+
- Buttons that are tiny on mobile.
|
|
488
|
+
- Inputs that auto-zoom on iOS because text size is below 16px.
|
|
489
|
+
- A `100vh` hero that overflows on iOS Safari.
|
|
490
|
+
- A surface that requires scrolling 12 screens on mobile while only 2 screens on desktop.
|
|
491
|
+
- Padding that's too tight on mobile (cramping content) or too generous on mobile (wasting space).
|
|
492
|
+
- Font that's too big on mobile (5 words per line) or too small (10pt body).
|
|
493
|
+
|
|
494
|
+
## Self-Healing for Responsive
|
|
495
|
+
|
|
496
|
+
Before declaring work complete:
|
|
497
|
+
|
|
498
|
+
- [ ] Tested at 320px, 375px, 768px, 1024px, 1280px, 1920px
|
|
499
|
+
- [ ] Tested in portrait and landscape on a phone
|
|
500
|
+
- [ ] No horizontal scroll at any width
|
|
501
|
+
- [ ] Body text >= 16px on mobile
|
|
502
|
+
- [ ] Touch targets >= 44x44 with 8px gaps
|
|
503
|
+
- [ ] All images, videos, iframes have max-width: 100%
|
|
504
|
+
- [ ] Long tables have horizontal scroll
|
|
505
|
+
- [ ] Sidebar collapses on small screens
|
|
506
|
+
- [ ] Modals fit smallest viewport
|
|
507
|
+
- [ ] 200% browser zoom: layout intact
|
|
508
|
+
- [ ] System text size at largest: layout intact
|
|
509
|
+
- [ ] Safe area respected on notched/island devices
|
|
510
|
+
- [ ] `100dvh` instead of `100vh` for full-height mobile
|
|
511
|
+
- [ ] Works at `prefers-reduced-motion: reduce`
|
|
512
|
+
- [ ] Works at `prefers-color-scheme: dark`
|
|
513
|
+
- [ ] Tested with keyboard only
|
|
514
|
+
|
|
515
|
+
## See Also
|
|
516
|
+
|
|
517
|
+
- [ui-ux.md](ui-ux.md) for the functional spacing/layout rules
|
|
518
|
+
- [design.md](design.md) for visual rhythm and grids
|
|
519
|
+
- [accessibility.md](accessibility.md) for accessible scaling
|