picasso-skill 2.4.0 → 2.6.0
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/agents/picasso.md +26 -542
- package/commands/godmode.md +3 -13
- package/commands/roast.md +3 -14
- package/commands/score.md +3 -18
- package/commands/steal.md +3 -31
- package/package.json +1 -1
- package/references/accessibility-wcag.md +3 -0
- package/references/code-typography.md +36 -166
- package/references/color-and-contrast.md +78 -345
- package/references/generative-art.md +49 -561
- package/references/modern-css-performance.md +46 -258
- package/references/motion-and-animation.md +225 -88
- package/references/navigation-patterns.md +29 -186
- package/references/performance-optimization.md +42 -678
- package/references/react-patterns.md +56 -216
- package/references/responsive-design.md +77 -379
- package/references/sensory-design.md +62 -263
- package/references/ux-writing.md +64 -354
- package/references/animation-performance.md +0 -244
- package/references/interaction-design.md +0 -162
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
2. Easing Functions
|
|
6
6
|
3. Duration Guidelines
|
|
7
7
|
4. Staggered Reveals
|
|
8
|
-
5.
|
|
9
|
-
6.
|
|
10
|
-
7.
|
|
11
|
-
8.
|
|
12
|
-
9. Common Mistakes
|
|
8
|
+
5. Text Morphing
|
|
9
|
+
6. Reduced Motion
|
|
10
|
+
7. Common Mistakes
|
|
11
|
+
8. Performance
|
|
13
12
|
|
|
14
13
|
---
|
|
15
14
|
|
|
@@ -125,42 +124,7 @@ const item = {
|
|
|
125
124
|
|
|
126
125
|
---
|
|
127
126
|
|
|
128
|
-
## 5.
|
|
129
|
-
|
|
130
|
-
### CSS-Only with Scroll-Driven Animations
|
|
131
|
-
```css
|
|
132
|
-
@keyframes fade-in {
|
|
133
|
-
from { opacity: 0; transform: translateY(20px); }
|
|
134
|
-
to { opacity: 1; transform: translateY(0); }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.scroll-reveal {
|
|
138
|
-
animation: fade-in linear both;
|
|
139
|
-
animation-timeline: view();
|
|
140
|
-
animation-range: entry 0% entry 30%;
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Intersection Observer Pattern
|
|
145
|
-
```js
|
|
146
|
-
const observer = new IntersectionObserver(
|
|
147
|
-
(entries) => {
|
|
148
|
-
entries.forEach(entry => {
|
|
149
|
-
if (entry.isIntersecting) {
|
|
150
|
-
entry.target.classList.add('visible');
|
|
151
|
-
observer.unobserve(entry.target);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
},
|
|
155
|
-
{ threshold: 0.15, rootMargin: '0px 0px -50px 0px' }
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
document.querySelectorAll('.animate-on-scroll').forEach(el => observer.observe(el));
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
## 6. Text Morphing
|
|
127
|
+
## 5. Text Morphing
|
|
164
128
|
|
|
165
129
|
For animated text transitions (changing labels, counters, status updates), use **Torph** (dependency-free).
|
|
166
130
|
|
|
@@ -193,52 +157,7 @@ Torph morphs individual characters with smooth enter/exit animations. It works w
|
|
|
193
157
|
|
|
194
158
|
---
|
|
195
159
|
|
|
196
|
-
##
|
|
197
|
-
|
|
198
|
-
### Button Press
|
|
199
|
-
```css
|
|
200
|
-
button:active {
|
|
201
|
-
transform: scale(0.97);
|
|
202
|
-
transition: transform 80ms var(--ease-in);
|
|
203
|
-
}
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
### Toggle Switch
|
|
207
|
-
Animate the knob position and background color simultaneously. The knob should arrive slightly before the color finishes changing.
|
|
208
|
-
|
|
209
|
-
### Checkbox
|
|
210
|
-
Scale the checkmark from 0 to 1 with a slight overshoot:
|
|
211
|
-
```css
|
|
212
|
-
.checkbox-mark {
|
|
213
|
-
transform: scale(0);
|
|
214
|
-
transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
215
|
-
}
|
|
216
|
-
.checkbox:checked .checkbox-mark {
|
|
217
|
-
transform: scale(1);
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Skeleton Loading
|
|
222
|
-
Shimmer from left to right using a gradient animation:
|
|
223
|
-
```css
|
|
224
|
-
.skeleton {
|
|
225
|
-
background: linear-gradient(90deg,
|
|
226
|
-
var(--surface-2) 25%,
|
|
227
|
-
var(--surface-3) 50%,
|
|
228
|
-
var(--surface-2) 75%
|
|
229
|
-
);
|
|
230
|
-
background-size: 200% 100%;
|
|
231
|
-
animation: shimmer 1.5s infinite;
|
|
232
|
-
}
|
|
233
|
-
@keyframes shimmer {
|
|
234
|
-
0% { background-position: 200% 0; }
|
|
235
|
-
100% { background-position: -200% 0; }
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## 8. Reduced Motion
|
|
160
|
+
## 6. Reduced Motion
|
|
242
161
|
|
|
243
162
|
Always respect the user's motion preference:
|
|
244
163
|
```css
|
|
@@ -256,7 +175,7 @@ This does not mean removing all visual feedback. Opacity changes (fades) are sti
|
|
|
256
175
|
|
|
257
176
|
---
|
|
258
177
|
|
|
259
|
-
##
|
|
178
|
+
## 7. Common Mistakes
|
|
260
179
|
|
|
261
180
|
- Animating everything on the page (creates visual noise, reduces perceived performance)
|
|
262
181
|
- Using `animation-duration: 0` for reduced motion (some browsers behave unexpectedly; use 0.01ms)
|
|
@@ -265,3 +184,221 @@ This does not mean removing all visual feedback. Opacity changes (fades) are sti
|
|
|
265
184
|
- Forgetting `will-change` on frequently animated elements (or overusing it on everything)
|
|
266
185
|
- Staggering 20+ items with visible delays (group them or animate the container)
|
|
267
186
|
- Using `transition: all 0.3s` (animates properties you did not intend; be explicit about which properties to transition)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 8. Performance
|
|
191
|
+
|
|
192
|
+
### Compositor-Only Properties
|
|
193
|
+
|
|
194
|
+
Only two CSS properties can be animated without triggering layout or paint: **transform** and **opacity**. Everything else causes reflow.
|
|
195
|
+
|
|
196
|
+
| Property | Layout | Paint | Composite | Animate? |
|
|
197
|
+
|---|---|---|---|---|
|
|
198
|
+
| `transform` | No | No | Yes | **Yes** |
|
|
199
|
+
| `opacity` | No | No | Yes | **Yes** |
|
|
200
|
+
| `filter` | No | Yes | Yes | Carefully |
|
|
201
|
+
| `background-color` | No | Yes | No | Avoid |
|
|
202
|
+
| `width`, `height` | Yes | Yes | No | **Never** |
|
|
203
|
+
| `top`, `left` | Yes | Yes | No | **Never** |
|
|
204
|
+
| `margin`, `padding` | Yes | Yes | No | **Never** |
|
|
205
|
+
| `border-radius` | No | Yes | No | Avoid |
|
|
206
|
+
|
|
207
|
+
```css
|
|
208
|
+
/* Good: compositor-only */
|
|
209
|
+
.slide-in {
|
|
210
|
+
transform: translateX(-100%);
|
|
211
|
+
opacity: 0;
|
|
212
|
+
transition: transform 300ms var(--ease-out), opacity 300ms var(--ease-out);
|
|
213
|
+
}
|
|
214
|
+
.slide-in.active {
|
|
215
|
+
transform: translateX(0);
|
|
216
|
+
opacity: 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Bad: triggers layout on every frame */
|
|
220
|
+
.slide-in-bad {
|
|
221
|
+
left: -100%;
|
|
222
|
+
transition: left 300ms ease;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Will-Change Best Practices
|
|
227
|
+
|
|
228
|
+
`will-change` promotes an element to its own compositor layer. This speeds up animation but consumes GPU memory.
|
|
229
|
+
|
|
230
|
+
Rules:
|
|
231
|
+
- Add `will-change` BEFORE the animation starts (e.g., on hover, not in the animation itself).
|
|
232
|
+
- Remove it AFTER the animation completes.
|
|
233
|
+
- Never use `will-change: all` -- it promotes everything.
|
|
234
|
+
- Never apply it to more than 10 elements simultaneously.
|
|
235
|
+
- Don't put it in your stylesheet permanently.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
// Good: apply before, remove after
|
|
239
|
+
element.addEventListener('mouseenter', () => {
|
|
240
|
+
element.style.willChange = 'transform';
|
|
241
|
+
});
|
|
242
|
+
element.addEventListener('transitionend', () => {
|
|
243
|
+
element.style.willChange = 'auto';
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```css
|
|
248
|
+
/* Acceptable: for elements that are ALWAYS animated (e.g., loading spinners) */
|
|
249
|
+
.spinner { will-change: transform; }
|
|
250
|
+
|
|
251
|
+
/* Bad: permanent will-change on static elements */
|
|
252
|
+
.card { will-change: transform, opacity; } /* don't do this */
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Layout Thrashing
|
|
256
|
+
|
|
257
|
+
Reading layout properties then writing them in a loop forces the browser to recalculate layout on every iteration.
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
// BAD: layout thrashing (read-write-read-write)
|
|
261
|
+
elements.forEach(el => {
|
|
262
|
+
const height = el.offsetHeight; // READ -- forces layout
|
|
263
|
+
el.style.height = height + 10 + 'px'; // WRITE -- invalidates layout
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// GOOD: batch reads, then batch writes
|
|
267
|
+
const heights = elements.map(el => el.offsetHeight); // all reads first
|
|
268
|
+
elements.forEach((el, i) => {
|
|
269
|
+
el.style.height = heights[i] + 10 + 'px'; // all writes after
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Properties that trigger forced layout when read: `offsetHeight`, `offsetWidth`, `getBoundingClientRect()`, `scrollTop`, `clientHeight`, `getComputedStyle()`.
|
|
274
|
+
|
|
275
|
+
### IntersectionObserver vs Scroll Events
|
|
276
|
+
|
|
277
|
+
| | `scroll` event | IntersectionObserver |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| Performance | Fires every pixel, blocks main thread | Async, fires on threshold crossing |
|
|
280
|
+
| Throttling | Manual (requestAnimationFrame) | Built-in |
|
|
281
|
+
| Use case | Scroll-linked animation (position) | Visibility detection (enter/exit) |
|
|
282
|
+
| Recommendation | Almost never | Almost always |
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
// Good: IntersectionObserver for reveal animations
|
|
286
|
+
const observer = new IntersectionObserver(
|
|
287
|
+
(entries) => {
|
|
288
|
+
entries.forEach(entry => {
|
|
289
|
+
entry.target.classList.toggle('visible', entry.isIntersecting);
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
{ threshold: 0.1 }
|
|
293
|
+
);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
For scroll-linked animations where you need exact scroll position, use the Scroll-Driven Animations API:
|
|
297
|
+
|
|
298
|
+
```css
|
|
299
|
+
@keyframes fade-in {
|
|
300
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
301
|
+
to { opacity: 1; transform: translateY(0); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.scroll-reveal {
|
|
305
|
+
animation: fade-in linear;
|
|
306
|
+
animation-timeline: view();
|
|
307
|
+
animation-range: entry 0% entry 100%;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Web Animations API
|
|
312
|
+
|
|
313
|
+
For complex JS-driven animations, use WAAPI instead of manual `requestAnimationFrame`:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
element.animate(
|
|
317
|
+
[
|
|
318
|
+
{ transform: 'translateY(20px)', opacity: 0 },
|
|
319
|
+
{ transform: 'translateY(0)', opacity: 1 }
|
|
320
|
+
],
|
|
321
|
+
{
|
|
322
|
+
duration: 300,
|
|
323
|
+
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
|
324
|
+
fill: 'forwards'
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Benefits over CSS: programmable, cancellable, can read progress, can reverse.
|
|
330
|
+
|
|
331
|
+
### Performance Measurement
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
// Measure animation frame rate
|
|
335
|
+
let frames = 0;
|
|
336
|
+
let lastTime = performance.now();
|
|
337
|
+
|
|
338
|
+
function countFrames() {
|
|
339
|
+
frames++;
|
|
340
|
+
const now = performance.now();
|
|
341
|
+
if (now - lastTime >= 1000) {
|
|
342
|
+
console.log(`FPS: ${frames}`);
|
|
343
|
+
frames = 0;
|
|
344
|
+
lastTime = now;
|
|
345
|
+
}
|
|
346
|
+
requestAnimationFrame(countFrames);
|
|
347
|
+
}
|
|
348
|
+
requestAnimationFrame(countFrames);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Chrome DevTools:
|
|
352
|
+
1. Performance tab: Record, interact with animations, Stop
|
|
353
|
+
2. Look for long frames (> 16ms) in the flame chart
|
|
354
|
+
3. Rendering tab: Paint flashing (green = repaint, should be minimal)
|
|
355
|
+
4. Rendering tab: Layer borders (orange = composited layers)
|
|
356
|
+
|
|
357
|
+
Target: 60fps = 16.67ms per frame. If ANY frame takes > 33ms, users perceive jank.
|
|
358
|
+
|
|
359
|
+
### Testing on Low-End Devices
|
|
360
|
+
|
|
361
|
+
Your M-series Mac is not representative. Test with:
|
|
362
|
+
- **Chrome DevTools CPU throttling:** Performance tab, CPU, 6x slowdown
|
|
363
|
+
- **Network throttling:** Slow 3G preset to test loading animations
|
|
364
|
+
- **Real device:** Test on a 3-year-old Android phone if possible
|
|
365
|
+
|
|
366
|
+
If an animation stutters at 6x CPU throttle, reduce:
|
|
367
|
+
1. Number of concurrent animations
|
|
368
|
+
2. Element count being animated
|
|
369
|
+
3. Complexity of each animation
|
|
370
|
+
|
|
371
|
+
### Contain Property
|
|
372
|
+
|
|
373
|
+
`contain` tells the browser what NOT to recalculate when an element changes.
|
|
374
|
+
|
|
375
|
+
```css
|
|
376
|
+
/* Isolate layout/paint to this element */
|
|
377
|
+
.card {
|
|
378
|
+
contain: layout paint;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/* Full isolation -- best for off-screen or independent components */
|
|
382
|
+
.widget {
|
|
383
|
+
contain: strict; /* = size + layout + paint + style */
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Content containment -- layout + paint + style (most common) */
|
|
387
|
+
.list-item {
|
|
388
|
+
contain: content;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Use `contain: content` on repeated elements (list items, cards) to prevent layout changes from propagating to siblings.
|
|
393
|
+
|
|
394
|
+
### Performance Common Mistakes
|
|
395
|
+
|
|
396
|
+
- **Animating `width`/`height`/`top`/`left`.** Use `transform: translate/scale` instead.
|
|
397
|
+
- **`will-change` on everything.** Max 10 elements. Remove after animation completes.
|
|
398
|
+
- **`addEventListener('scroll')` for visibility.** Use IntersectionObserver.
|
|
399
|
+
- **Reading layout properties in animation loops.** Batch reads before writes.
|
|
400
|
+
- **Testing only on fast hardware.** Use Chrome CPU throttling at 6x.
|
|
401
|
+
- **No frame budget awareness.** 16ms per frame. If your JS takes 20ms, you drop frames.
|
|
402
|
+
- **CSS `transition: all`.** Transitions every property including layout triggers.
|
|
403
|
+
- **Forgetting `contain` on repeated elements.** One card's layout change recalculates the entire list.
|
|
404
|
+
- **`requestAnimationFrame` without cancellation.** Always store the ID and `cancelAnimationFrame` on cleanup.
|
|
@@ -1,36 +1,10 @@
|
|
|
1
1
|
# Navigation Patterns Reference
|
|
2
2
|
|
|
3
|
-
## Table of Contents
|
|
4
|
-
1. Breadcrumb Systems
|
|
5
|
-
2. Sidebar and Drawer Navigation
|
|
6
|
-
3. Tab Navigation vs Button Groups
|
|
7
|
-
4. Mobile Bottom Bar vs Hamburger
|
|
8
|
-
5. Sticky Headers
|
|
9
|
-
6. Mega Menus
|
|
10
|
-
7. Skip Links
|
|
11
|
-
8. Common Mistakes
|
|
12
|
-
|
|
13
3
|
---
|
|
14
4
|
|
|
15
5
|
## 1. Breadcrumb Systems
|
|
16
6
|
|
|
17
|
-
|
|
18
|
-
<nav aria-label="Breadcrumb">
|
|
19
|
-
<ol class="flex items-center gap-1.5 text-sm">
|
|
20
|
-
<li><a href="/" class="text-muted hover:text-primary">Home</a></li>
|
|
21
|
-
<li aria-hidden="true" class="text-muted">/</li>
|
|
22
|
-
<li><a href="/products" class="text-muted hover:text-primary">Products</a></li>
|
|
23
|
-
<li aria-hidden="true" class="text-muted">/</li>
|
|
24
|
-
<li><span aria-current="page" class="text-primary font-medium">Widget Pro</span></li>
|
|
25
|
-
</ol>
|
|
26
|
-
</nav>
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Rules:
|
|
30
|
-
- Use `<nav aria-label="Breadcrumb">` with `<ol>` (ordered list).
|
|
31
|
-
- Mark current page with `aria-current="page"`. Don't make it a link.
|
|
32
|
-
- Separator (`/` or `>`) gets `aria-hidden="true"`.
|
|
33
|
-
- Show 3-5 levels max. Truncate middle levels with `...` on mobile.
|
|
7
|
+
Use `<nav aria-label="Breadcrumb">` with `<ol>`. Mark current page with `aria-current="page"` (not a link). Separators get `aria-hidden="true"`. Show 3-5 levels max; truncate middle on mobile.
|
|
34
8
|
|
|
35
9
|
---
|
|
36
10
|
|
|
@@ -38,23 +12,7 @@ Rules:
|
|
|
38
12
|
|
|
39
13
|
Desktop: fixed sidebar, 240-280px width. Mobile: off-canvas drawer with overlay.
|
|
40
14
|
|
|
41
|
-
|
|
42
|
-
<aside className={`fixed inset-y-0 left-0 w-[260px] bg-surface-1 border-r border-border
|
|
43
|
-
transform transition-transform duration-200 z-50
|
|
44
|
-
${mobileOpen ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0`}>
|
|
45
|
-
<nav aria-label="Main navigation">
|
|
46
|
-
{/* nav items */}
|
|
47
|
-
</nav>
|
|
48
|
-
</aside>
|
|
49
|
-
|
|
50
|
-
{/* Mobile overlay */}
|
|
51
|
-
{mobileOpen && (
|
|
52
|
-
<div className="fixed inset-0 bg-black/50 z-40 md:hidden"
|
|
53
|
-
onClick={() => setMobileOpen(false)} />
|
|
54
|
-
)}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Active state: use background tint + accent color text, not bold weight (weight change shifts text width).
|
|
15
|
+
Active state: background tint + accent color text (not bold weight, which shifts text width).
|
|
58
16
|
|
|
59
17
|
```css
|
|
60
18
|
.nav-item { padding: 7px 12px; border-radius: 6px; transition: background 100ms; }
|
|
@@ -62,186 +20,71 @@ Active state: use background tint + accent color text, not bold weight (weight c
|
|
|
62
20
|
.nav-item.active { background: oklch(0.65 0.15 230 / 0.1); color: var(--accent); }
|
|
63
21
|
```
|
|
64
22
|
|
|
65
|
-
Section labels: 10px, uppercase, `tracking-[0.12em]`, muted color. Group related items
|
|
23
|
+
Section labels: 10px, uppercase, `tracking-[0.12em]`, muted color. Group related items.
|
|
66
24
|
|
|
67
25
|
---
|
|
68
26
|
|
|
69
27
|
## 3. Tab Navigation vs Button Groups
|
|
70
28
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<button role="tab" aria-selected="true" aria-controls="panel-profile" id="tab-profile">
|
|
76
|
-
Profile
|
|
77
|
-
</button>
|
|
78
|
-
<button role="tab" aria-selected="false" aria-controls="panel-billing" id="tab-billing">
|
|
79
|
-
Billing
|
|
80
|
-
</button>
|
|
81
|
-
</div>
|
|
82
|
-
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
|
|
83
|
-
<!-- content -->
|
|
84
|
-
</div>
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
**Button groups:** trigger independent ACTIONS. Multiple can be active (toggle group) or none.
|
|
88
|
-
|
|
89
|
-
```html
|
|
90
|
-
<div role="group" aria-label="View options">
|
|
91
|
-
<button aria-pressed="true">Grid</button>
|
|
92
|
-
<button aria-pressed="false">List</button>
|
|
93
|
-
</div>
|
|
94
|
-
```
|
|
29
|
+
| Pattern | Purpose | ARIA | Selection |
|
|
30
|
+
|---------|---------|------|-----------|
|
|
31
|
+
| Tabs | Switch views of SAME content | `role="tablist"` / `role="tab"` / `aria-selected` | One always active |
|
|
32
|
+
| Button groups | Trigger independent actions | `role="group"` / `aria-pressed` | Multiple or none active |
|
|
95
33
|
|
|
96
|
-
Decision:
|
|
34
|
+
Decision: clicking switches what you see = tabs. Clicking does something = button group.
|
|
97
35
|
|
|
98
36
|
---
|
|
99
37
|
|
|
100
38
|
## 4. Mobile Bottom Bar vs Hamburger
|
|
101
39
|
|
|
102
|
-
|
|
40
|
+
| Criteria | Bottom Bar | Hamburger |
|
|
41
|
+
|----------|------------|-----------|
|
|
42
|
+
| Destinations | 3-5 primary | 6+ items |
|
|
43
|
+
| App type | Mobile-first app | Marketing/content site |
|
|
44
|
+
| Frequency | Frequent switching | Infrequent navigation |
|
|
103
45
|
|
|
104
|
-
|
|
105
|
-
- 3-5 primary destinations
|
|
106
|
-
- Mobile-first app (not marketing site)
|
|
107
|
-
- User needs to switch between sections frequently
|
|
108
|
-
|
|
109
|
-
**When to use hamburger:**
|
|
110
|
-
- 6+ navigation items
|
|
111
|
-
- Marketing/content site with hierarchical nav
|
|
112
|
-
- Secondary navigation (settings, help)
|
|
113
|
-
|
|
114
|
-
```css
|
|
115
|
-
.bottom-bar {
|
|
116
|
-
position: fixed;
|
|
117
|
-
bottom: 0;
|
|
118
|
-
left: 0;
|
|
119
|
-
right: 0;
|
|
120
|
-
height: 56px; /* 48px min for touch, 56px comfortable */
|
|
121
|
-
padding-bottom: env(safe-area-inset-bottom); /* notch-safe */
|
|
122
|
-
display: flex;
|
|
123
|
-
justify-content: space-around;
|
|
124
|
-
align-items: center;
|
|
125
|
-
background: var(--surface-1);
|
|
126
|
-
border-top: 1px solid var(--border);
|
|
127
|
-
z-index: 50;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.bottom-bar-item {
|
|
131
|
-
display: flex;
|
|
132
|
-
flex-direction: column;
|
|
133
|
-
align-items: center;
|
|
134
|
-
gap: 2px;
|
|
135
|
-
min-width: 64px;
|
|
136
|
-
padding: 4px 12px;
|
|
137
|
-
font-size: 10px;
|
|
138
|
-
}
|
|
139
|
-
```
|
|
46
|
+
Bottom bar: `position: fixed; bottom: 0; height: 56px; padding-bottom: env(safe-area-inset-bottom);`
|
|
140
47
|
|
|
141
48
|
---
|
|
142
49
|
|
|
143
50
|
## 5. Sticky Headers
|
|
144
51
|
|
|
145
|
-
Keep slim: 48-56px height. Hide on scroll-down, reveal on scroll-up.
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
let lastScroll = 0;
|
|
149
|
-
const header = document.querySelector('header');
|
|
150
|
-
|
|
151
|
-
window.addEventListener('scroll', () => {
|
|
152
|
-
const current = window.scrollY;
|
|
153
|
-
if (current > lastScroll && current > 80) {
|
|
154
|
-
header.style.transform = 'translateY(-100%)';
|
|
155
|
-
} else {
|
|
156
|
-
header.style.transform = 'translateY(0)';
|
|
157
|
-
}
|
|
158
|
-
lastScroll = current;
|
|
159
|
-
}, { passive: true });
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
```css
|
|
163
|
-
header {
|
|
164
|
-
position: sticky;
|
|
165
|
-
top: 0;
|
|
166
|
-
height: 56px;
|
|
167
|
-
z-index: 40;
|
|
168
|
-
backdrop-filter: blur(12px);
|
|
169
|
-
background: oklch(var(--bg-l) var(--bg-c) var(--bg-h) / 0.8);
|
|
170
|
-
border-bottom: 1px solid var(--border);
|
|
171
|
-
transition: transform 200ms ease-out;
|
|
172
|
-
}
|
|
173
|
-
```
|
|
52
|
+
Keep slim: 48-56px height. Hide on scroll-down, reveal on scroll-up (track `scrollY` delta). Use `backdrop-filter: blur(12px)` with semi-transparent background. Add `{ passive: true }` to scroll listener.
|
|
174
53
|
|
|
175
54
|
---
|
|
176
55
|
|
|
177
56
|
## 6. Mega Menus
|
|
178
57
|
|
|
179
|
-
For sites with 20+ navigation items.
|
|
180
|
-
|
|
181
|
-
**Full-width panel** (preferred): Flyout spans full viewport width. Organized in columns.
|
|
182
|
-
|
|
183
|
-
```css
|
|
184
|
-
.mega-menu {
|
|
185
|
-
position: absolute;
|
|
186
|
-
top: 100%;
|
|
187
|
-
left: 0;
|
|
188
|
-
right: 0;
|
|
189
|
-
padding: 2rem;
|
|
190
|
-
display: grid;
|
|
191
|
-
grid-template-columns: repeat(4, 1fr);
|
|
192
|
-
gap: 2rem;
|
|
193
|
-
background: var(--surface-1);
|
|
194
|
-
border-top: 1px solid var(--border);
|
|
195
|
-
box-shadow: var(--shadow-lg);
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
**Alternatives to mega menus:**
|
|
200
|
-
- Search-first navigation (Command+K palette)
|
|
201
|
-
- Two-level sidebar (category → items)
|
|
202
|
-
- Card-based navigation page
|
|
58
|
+
For sites with 20+ navigation items. Full-width panel with 4-column grid is preferred. Keep hover delay at 200-300ms to prevent accidental opens.
|
|
203
59
|
|
|
204
|
-
|
|
60
|
+
**Alternatives**: Command+K palette, two-level sidebar, card-based navigation page.
|
|
205
61
|
|
|
206
62
|
---
|
|
207
63
|
|
|
208
64
|
## 7. Skip Links
|
|
209
65
|
|
|
210
|
-
First focusable element on the page. Visually hidden until focused
|
|
66
|
+
First focusable element on the page. Visually hidden until focused:
|
|
211
67
|
|
|
212
68
|
```html
|
|
213
69
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
214
|
-
<!-- header, nav, etc -->
|
|
215
70
|
<main id="main-content" tabindex="-1">
|
|
216
71
|
```
|
|
217
72
|
|
|
218
73
|
```css
|
|
219
|
-
.skip-link {
|
|
220
|
-
|
|
221
|
-
top: -100%;
|
|
222
|
-
left: 1rem;
|
|
223
|
-
z-index: 100;
|
|
224
|
-
padding: 0.5rem 1rem;
|
|
225
|
-
background: var(--accent);
|
|
226
|
-
color: white;
|
|
227
|
-
border-radius: 0 0 8px 8px;
|
|
228
|
-
font-weight: 600;
|
|
229
|
-
}
|
|
230
|
-
.skip-link:focus {
|
|
231
|
-
top: 0;
|
|
232
|
-
}
|
|
74
|
+
.skip-link { position: absolute; top: -100%; z-index: 100; }
|
|
75
|
+
.skip-link:focus { top: 0; }
|
|
233
76
|
```
|
|
234
77
|
|
|
235
78
|
---
|
|
236
79
|
|
|
237
80
|
## 8. Common Mistakes
|
|
238
81
|
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
-
|
|
82
|
+
- Hamburger as the only mobile navigation (bottom bar is better for primary destinations)
|
|
83
|
+
- Active state using `font-weight: bold` (shifts text width; use background/color)
|
|
84
|
+
- Sidebar over 300px wide (240-280px is ideal)
|
|
85
|
+
- Sticky header over 64px (48-56px max)
|
|
86
|
+
- No `aria-current="page"` on breadcrumbs
|
|
87
|
+
- Tabs without `role="tablist"`/`role="tab"`
|
|
88
|
+
- Mega menu without hover delay
|
|
89
|
+
- No skip link
|
|
90
|
+
- Bottom bar without `safe-area-inset-bottom`
|