picasso-skill 2.0.0 → 2.0.2
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/README.md +67 -65
- package/agents/picasso.md +36 -15
- package/bin/install.mjs +1 -1
- package/package.json +2 -2
- package/references/animation-performance.md +244 -0
- package/references/brand-and-identity.md +136 -0
- package/references/code-typography.md +222 -0
- package/references/dark-mode.md +199 -0
- package/references/i18n-visual-patterns.md +177 -0
- package/references/images-and-media.md +222 -0
- package/references/loading-and-states.md +258 -0
- package/references/micro-interactions.md +291 -0
- package/references/navigation-patterns.md +247 -0
- package/references/tables-and-forms.md +227 -0
- package/skills/picasso/SKILL.md +10 -1
- package/templates/picasso-config.md +53 -0
- package/checklists/pre-ship.md +0 -83
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Micro-Interactions Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. Scroll-Triggered Animations
|
|
5
|
+
2. Page Transitions
|
|
6
|
+
3. Button and Press Feedback
|
|
7
|
+
4. Toggle and Switch Animations
|
|
8
|
+
5. Number and Text Morphing
|
|
9
|
+
6. Magnetic and Cursor Effects
|
|
10
|
+
7. Gesture-Based Interactions
|
|
11
|
+
8. Animation Performance Budget
|
|
12
|
+
9. Common Mistakes
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Scroll-Triggered Animations
|
|
17
|
+
|
|
18
|
+
Use IntersectionObserver. Never `addEventListener('scroll')` — it fires on every pixel and destroys performance.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const observer = new IntersectionObserver((entries) => {
|
|
22
|
+
entries.forEach(entry => {
|
|
23
|
+
if (entry.isIntersecting) {
|
|
24
|
+
entry.target.classList.add('revealed');
|
|
25
|
+
observer.unobserve(entry.target); // animate once
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}, { threshold: 0.15, rootMargin: '0px 0px -50px 0px' });
|
|
29
|
+
|
|
30
|
+
document.querySelectorAll('.reveal-on-scroll').forEach(el => observer.observe(el));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
.reveal-on-scroll {
|
|
35
|
+
opacity: 0;
|
|
36
|
+
transform: translateY(20px);
|
|
37
|
+
transition: opacity 0.5s var(--ease-out), transform 0.5s var(--ease-out);
|
|
38
|
+
}
|
|
39
|
+
.reveal-on-scroll.revealed {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
transform: translateY(0);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Parallax: max 10% speed difference. More than that causes motion sickness.
|
|
46
|
+
|
|
47
|
+
```css
|
|
48
|
+
.parallax-slow { transform: translateY(calc(var(--scroll-y) * -0.05)); }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. Page Transitions
|
|
54
|
+
|
|
55
|
+
View Transitions API for smooth page-to-page navigation:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
::view-transition-old(root) {
|
|
59
|
+
animation: fade-out 200ms ease-in forwards;
|
|
60
|
+
}
|
|
61
|
+
::view-transition-new(root) {
|
|
62
|
+
animation: fade-in 300ms ease-out forwards;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes fade-out { to { opacity: 0; } }
|
|
66
|
+
@keyframes fade-in { from { opacity: 0; } }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// In Next.js App Router or SPA
|
|
71
|
+
if (document.startViewTransition) {
|
|
72
|
+
document.startViewTransition(() => {
|
|
73
|
+
// Update DOM here
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Shared element transitions for items that persist between pages:
|
|
79
|
+
|
|
80
|
+
```css
|
|
81
|
+
.product-image { view-transition-name: product-hero; }
|
|
82
|
+
|
|
83
|
+
::view-transition-old(product-hero),
|
|
84
|
+
::view-transition-new(product-hero) {
|
|
85
|
+
animation-duration: 300ms;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. Button and Press Feedback
|
|
92
|
+
|
|
93
|
+
Combine scale + shadow reduction for a physical "press" feel:
|
|
94
|
+
|
|
95
|
+
```css
|
|
96
|
+
.btn {
|
|
97
|
+
transition: transform 80ms ease-out, box-shadow 80ms ease-out;
|
|
98
|
+
}
|
|
99
|
+
.btn:active {
|
|
100
|
+
transform: scale(0.97);
|
|
101
|
+
box-shadow: 0 1px 2px oklch(0 0 0 / 0.1); /* shadow shrinks on press */
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For icon buttons, use a radial ripple:
|
|
106
|
+
|
|
107
|
+
```css
|
|
108
|
+
.icon-btn {
|
|
109
|
+
position: relative;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
}
|
|
112
|
+
.icon-btn::after {
|
|
113
|
+
content: '';
|
|
114
|
+
position: absolute;
|
|
115
|
+
inset: 0;
|
|
116
|
+
background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), oklch(1 0 0 / 0.1) 0%, transparent 60%);
|
|
117
|
+
opacity: 0;
|
|
118
|
+
transition: opacity 150ms;
|
|
119
|
+
}
|
|
120
|
+
.icon-btn:active::after { opacity: 1; }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 4. Toggle and Switch Animations
|
|
126
|
+
|
|
127
|
+
```css
|
|
128
|
+
.toggle-track {
|
|
129
|
+
width: 44px;
|
|
130
|
+
height: 24px;
|
|
131
|
+
border-radius: 12px;
|
|
132
|
+
background: var(--surface-3);
|
|
133
|
+
transition: background-color 200ms ease-out;
|
|
134
|
+
position: relative;
|
|
135
|
+
}
|
|
136
|
+
.toggle-track[aria-checked="true"] {
|
|
137
|
+
background: var(--accent);
|
|
138
|
+
}
|
|
139
|
+
.toggle-thumb {
|
|
140
|
+
width: 20px;
|
|
141
|
+
height: 20px;
|
|
142
|
+
border-radius: 50%;
|
|
143
|
+
background: white;
|
|
144
|
+
position: absolute;
|
|
145
|
+
top: 2px;
|
|
146
|
+
left: 2px;
|
|
147
|
+
transition: transform 200ms var(--ease-out);
|
|
148
|
+
box-shadow: 0 1px 3px oklch(0 0 0 / 0.15);
|
|
149
|
+
}
|
|
150
|
+
.toggle-track[aria-checked="true"] .toggle-thumb {
|
|
151
|
+
transform: translateX(20px);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Checkbox checkmark: draw with SVG stroke-dasharray animation (150ms).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 5. Number and Text Morphing
|
|
160
|
+
|
|
161
|
+
For counters and changing text, use `torph` or CSS counter animation:
|
|
162
|
+
|
|
163
|
+
```css
|
|
164
|
+
@property --num {
|
|
165
|
+
syntax: '<integer>';
|
|
166
|
+
initial-value: 0;
|
|
167
|
+
inherits: false;
|
|
168
|
+
}
|
|
169
|
+
.counter {
|
|
170
|
+
transition: --num 1s ease-out;
|
|
171
|
+
counter-reset: num var(--num);
|
|
172
|
+
}
|
|
173
|
+
.counter::after {
|
|
174
|
+
content: counter(num);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
For text morphing between labels (e.g., tab switching):
|
|
179
|
+
|
|
180
|
+
```jsx
|
|
181
|
+
import { TextMorph } from 'torph/react';
|
|
182
|
+
|
|
183
|
+
<TextMorph>{activeTab === 'overview' ? 'Overview' : 'Details'}</TextMorph>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 6. Magnetic and Cursor Effects
|
|
189
|
+
|
|
190
|
+
Subtle magnetic pull toward buttons/links. Max displacement: 8px. Only on desktop (no hover on mobile).
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// Apply to elements with data-magnetic
|
|
194
|
+
document.querySelectorAll('[data-magnetic]').forEach(el => {
|
|
195
|
+
el.addEventListener('mousemove', (e) => {
|
|
196
|
+
const rect = el.getBoundingClientRect();
|
|
197
|
+
const x = e.clientX - rect.left - rect.width / 2;
|
|
198
|
+
const y = e.clientY - rect.top - rect.height / 2;
|
|
199
|
+
el.style.transform = `translate(${x * 0.15}px, ${y * 0.15}px)`;
|
|
200
|
+
});
|
|
201
|
+
el.addEventListener('mouseleave', () => {
|
|
202
|
+
el.style.transform = '';
|
|
203
|
+
el.style.transition = 'transform 0.3s var(--ease-out)';
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Custom cursor (use sparingly — only for creative/portfolio sites):
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
.custom-cursor {
|
|
212
|
+
cursor: none;
|
|
213
|
+
}
|
|
214
|
+
.cursor-dot {
|
|
215
|
+
width: 8px; height: 8px;
|
|
216
|
+
background: var(--accent);
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
position: fixed;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
z-index: 9999;
|
|
221
|
+
transition: transform 0.1s;
|
|
222
|
+
mix-blend-mode: difference;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 7. Gesture-Based Interactions
|
|
229
|
+
|
|
230
|
+
Mobile gestures require `touch-action` CSS to prevent browser defaults:
|
|
231
|
+
|
|
232
|
+
```css
|
|
233
|
+
/* Horizontal swipe area — disable horizontal scroll */
|
|
234
|
+
.swipe-container { touch-action: pan-y; }
|
|
235
|
+
|
|
236
|
+
/* Pinch-to-zoom area */
|
|
237
|
+
.zoomable { touch-action: manipulation; }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Swipe detection (vanilla, no library):
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
let startX = 0;
|
|
244
|
+
el.addEventListener('touchstart', e => { startX = e.touches[0].clientX; });
|
|
245
|
+
el.addEventListener('touchend', e => {
|
|
246
|
+
const diff = e.changedTouches[0].clientX - startX;
|
|
247
|
+
if (Math.abs(diff) > 50) { // 50px threshold
|
|
248
|
+
diff > 0 ? onSwipeRight() : onSwipeLeft();
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Gesture discoverability: NEVER rely on hidden gestures. Always provide a visible button alternative. Swipe should be a shortcut, not the only way.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 8. Animation Performance Budget
|
|
258
|
+
|
|
259
|
+
- **Max 3 concurrent animations** on screen at once. More causes frame drops on mid-range devices.
|
|
260
|
+
- **Only animate `transform` and `opacity`** — these are compositor-only and skip layout/paint.
|
|
261
|
+
- **Use `will-change` sparingly** — add before animation starts, remove after. Never `will-change: all`.
|
|
262
|
+
- **16ms frame budget** — if your animation JS takes more than 16ms per frame, it drops below 60fps.
|
|
263
|
+
- **Test with CPU throttling** — Chrome DevTools → Performance → CPU 6x slowdown. If it stutters there, it stutters on real phones.
|
|
264
|
+
|
|
265
|
+
```css
|
|
266
|
+
/* Good: compositor-only */
|
|
267
|
+
.animate-enter {
|
|
268
|
+
will-change: transform, opacity;
|
|
269
|
+
animation: slide-in 300ms var(--ease-out) forwards;
|
|
270
|
+
}
|
|
271
|
+
.animate-enter.done { will-change: auto; } /* remove after animation */
|
|
272
|
+
|
|
273
|
+
@keyframes slide-in {
|
|
274
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
275
|
+
to { opacity: 1; transform: translateY(0); }
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 9. Common Mistakes
|
|
282
|
+
|
|
283
|
+
- **`addEventListener('scroll')` for animations.** Use IntersectionObserver.
|
|
284
|
+
- **Animating `width`, `height`, `top`, `left`.** Triggers layout. Use `transform` only.
|
|
285
|
+
- **More than 3 concurrent animations.** Causes jank on real devices.
|
|
286
|
+
- **`will-change` on everything.** Creates a compositing layer per element. Memory hog.
|
|
287
|
+
- **Parallax with large speed differences.** Max 10% or users get motion sick.
|
|
288
|
+
- **Hidden gestures with no visible alternative.** Users won't discover them.
|
|
289
|
+
- **No `prefers-reduced-motion` check.** Wrap all non-essential motion in the media query.
|
|
290
|
+
- **Magnetic effects on mobile.** No hover on touch devices. Gate with `@media (hover: hover)`.
|
|
291
|
+
- **Page transitions without fallback.** View Transitions API isn't universal. Always provide a no-transition fallback.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Navigation Patterns Reference
|
|
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
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Breadcrumb Systems
|
|
16
|
+
|
|
17
|
+
```html
|
|
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.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Sidebar and Drawer Navigation
|
|
38
|
+
|
|
39
|
+
Desktop: fixed sidebar, 240-280px width. Mobile: off-canvas drawer with overlay.
|
|
40
|
+
|
|
41
|
+
```jsx
|
|
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).
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
.nav-item { padding: 7px 12px; border-radius: 6px; transition: background 100ms; }
|
|
61
|
+
.nav-item:hover { background: oklch(1 0 0 / 0.04); }
|
|
62
|
+
.nav-item.active { background: oklch(0.65 0.15 230 / 0.1); color: var(--accent); }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Section labels: 10px, uppercase, `tracking-[0.12em]`, muted color. Group related items visually.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 3. Tab Navigation vs Button Groups
|
|
70
|
+
|
|
71
|
+
**Tabs:** switch between views of the SAME content. One is always active. Use `role="tablist"`.
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<div role="tablist" aria-label="Account sections">
|
|
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
|
+
```
|
|
95
|
+
|
|
96
|
+
Decision: if clicking switches what you see → tabs. If clicking does something → button group.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 4. Mobile Bottom Bar vs Hamburger
|
|
101
|
+
|
|
102
|
+
Bottom bar outperforms hamburger for engagement. Hamburger hides navigation behind a tap, reducing discoverability.
|
|
103
|
+
|
|
104
|
+
**When to use bottom bar:**
|
|
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
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 5. Sticky Headers
|
|
144
|
+
|
|
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
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 6. Mega Menus
|
|
178
|
+
|
|
179
|
+
For sites with 20+ navigation items. Two approaches:
|
|
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
|
|
203
|
+
|
|
204
|
+
Keep mega menu hover delay at 200-300ms to prevent accidental opens.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 7. Skip Links
|
|
209
|
+
|
|
210
|
+
First focusable element on the page. Visually hidden until focused.
|
|
211
|
+
|
|
212
|
+
```html
|
|
213
|
+
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
214
|
+
<!-- header, nav, etc -->
|
|
215
|
+
<main id="main-content" tabindex="-1">
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
.skip-link {
|
|
220
|
+
position: absolute;
|
|
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
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## 8. Common Mistakes
|
|
238
|
+
|
|
239
|
+
- **Hamburger as the only mobile navigation.** Bottom bar is better for primary destinations.
|
|
240
|
+
- **Active nav state using `font-weight: bold`.** Shifts text width. Use background/color instead.
|
|
241
|
+
- **Sidebar over 300px wide.** Steals too much screen. 240-280px is ideal.
|
|
242
|
+
- **Sticky header over 64px.** Takes too much vertical space. 48-56px max.
|
|
243
|
+
- **No `aria-current="page"` on breadcrumbs.** Screen readers can't identify current page.
|
|
244
|
+
- **Tabs without `role="tablist"`/`role="tab"`.** Not accessible.
|
|
245
|
+
- **Mega menu without hover delay.** Opens accidentally when moving mouse across nav.
|
|
246
|
+
- **No skip link.** Keyboard users must tab through entire header to reach content.
|
|
247
|
+
- **Bottom bar without `safe-area-inset-bottom`.** Content hides behind iPhone notch.
|