arkaos 5.4.0 → 5.5.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/README.md +2 -2
- package/THE-ARKAOS-GUIDE.md +1 -1
- package/VERSION +1 -1
- package/arka/SKILL.md +1 -1
- package/config/skills-provenance.yaml +18 -0
- package/departments/dev/skills/canvas-generative/SKILL.md +362 -0
- package/departments/dev/skills/canvas-generative/references/algorithms.md +934 -0
- package/departments/dev/skills/canvas-generative/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/css-native/SKILL.md +353 -0
- package/departments/dev/skills/css-native/references/genjutsu.LICENSE +21 -0
- package/departments/dev/skills/css-native/references/modern-css.md +465 -0
- package/departments/dev/skills/framer-motion/SKILL.md +223 -0
- package/departments/dev/skills/framer-motion/references/api.md +312 -0
- package/departments/dev/skills/framer-motion/references/genjutsu.LICENSE +21 -0
- package/harness/codex/AGENTS.md +1 -1
- package/harness/copilot/copilot-instructions.md +1 -1
- package/harness/cursor/rules/arkaos.mdc +2 -2
- package/harness/gemini/GEMINI.md +1 -1
- package/harness/opencode/AGENTS.md +1 -1
- package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
- package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
- package/harness/opencode/agents/arka-cfo-helena.md +1 -1
- package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
- package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
- package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
- package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
- package/harness/opencode/agents/arka-coo-sofia.md +1 -1
- package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
- package/harness/opencode/agents/arka-cqo-marta.md +1 -1
- package/harness/opencode/agents/arka-cto-marco.md +1 -1
- package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
- package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
- package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
- package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
- package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
- package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
- package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
- package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
- package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
- package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
- package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
- package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
- package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
- package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
- package/harness/zed/.rules +1 -1
- package/knowledge/commands-registry.json +1 -1
- package/knowledge/skills-manifest.json +46 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/marketplace_gen.py +1 -1
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
# Modern CSS — Browser Support, Fallbacks & Progressive Enhancement
|
|
2
|
+
|
|
3
|
+
> Last verified: March 2026
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Browser Support Tables
|
|
8
|
+
|
|
9
|
+
### Scroll-Driven Animations (`animation-timeline`)
|
|
10
|
+
|
|
11
|
+
| Browser | Version | Status |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Chrome | 115+ | Supported |
|
|
14
|
+
| Edge | 115+ | Supported |
|
|
15
|
+
| Firefox | 128+ | Supported (shipped July 2024) |
|
|
16
|
+
| Safari | 18.4+ | Supported (shipped early 2025) |
|
|
17
|
+
|
|
18
|
+
**Global coverage**: ~90%+ (as of March 2026)
|
|
19
|
+
|
|
20
|
+
### View Transitions API
|
|
21
|
+
|
|
22
|
+
| Feature | Chrome | Edge | Firefox | Safari |
|
|
23
|
+
|---|---|---|---|---|
|
|
24
|
+
| Same-document (`startViewTransition`) | 111+ | 111+ | 133+ | 18+ |
|
|
25
|
+
| Cross-document (`@view-transition`) | 126+ | 126+ | Not yet | 18.2+ |
|
|
26
|
+
| `view-transition-class` | 125+ | 125+ | Not yet | 18.2+ |
|
|
27
|
+
|
|
28
|
+
**Note**: Cross-document view transitions are an Interop 2026 focus area — Firefox support expected to land in 2026.
|
|
29
|
+
|
|
30
|
+
### @starting-style
|
|
31
|
+
|
|
32
|
+
| Browser | Version | Status |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Chrome | 117+ | Supported |
|
|
35
|
+
| Edge | 117+ | Supported |
|
|
36
|
+
| Firefox | 129+ | Supported |
|
|
37
|
+
| Safari | 17.5+ | Supported |
|
|
38
|
+
|
|
39
|
+
**Global coverage**: ~93%+ — Baseline Newly Available (2024)
|
|
40
|
+
|
|
41
|
+
### CSS Anchor Positioning
|
|
42
|
+
|
|
43
|
+
| Browser | Version | Status |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Chrome | 125+ | Supported |
|
|
46
|
+
| Edge | 125+ | Supported |
|
|
47
|
+
| Firefox | Nightly | In development (Interop 2026) |
|
|
48
|
+
| Safari | 26+ | Supported (shipped 2026) |
|
|
49
|
+
|
|
50
|
+
**Note**: Anchor positioning is an Interop 2026 focus area. Firefox expected to ship stable support in 2026.
|
|
51
|
+
|
|
52
|
+
### Container Queries
|
|
53
|
+
|
|
54
|
+
| Feature | Chrome | Edge | Firefox | Safari |
|
|
55
|
+
|---|---|---|---|---|
|
|
56
|
+
| Size queries (`@container`) | 105+ | 105+ | 110+ | 16+ |
|
|
57
|
+
| Container-relative units (`cqw`, `cqh`) | 105+ | 105+ | 110+ | 16+ |
|
|
58
|
+
| Style queries (`@container style()`) | 111+ | 111+ | Not yet | 18+ |
|
|
59
|
+
|
|
60
|
+
**Global coverage (size queries)**: ~95%+
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Fallback Patterns
|
|
65
|
+
|
|
66
|
+
### Scroll-Driven Animations
|
|
67
|
+
|
|
68
|
+
```css
|
|
69
|
+
/* Base: no scroll animation, element is always visible */
|
|
70
|
+
.reveal {
|
|
71
|
+
opacity: 1;
|
|
72
|
+
transform: translateY(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Progressive enhancement for supporting browsers */
|
|
76
|
+
@supports (animation-timeline: scroll()) {
|
|
77
|
+
.reveal {
|
|
78
|
+
animation: reveal-up linear both;
|
|
79
|
+
animation-timeline: view();
|
|
80
|
+
animation-range: entry 10% entry 90%;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes reveal-up {
|
|
84
|
+
from {
|
|
85
|
+
opacity: 0;
|
|
86
|
+
transform: translateY(2rem);
|
|
87
|
+
}
|
|
88
|
+
to {
|
|
89
|
+
opacity: 1;
|
|
90
|
+
transform: translateY(0);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Strategy**: Content is always visible by default. Scroll animation is purely decorative enhancement. Never hide content behind a scroll-driven animation without fallback.
|
|
97
|
+
|
|
98
|
+
### View Transitions (Same-Document)
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
function navigate(updateFn) {
|
|
102
|
+
// Fallback: just update the DOM instantly
|
|
103
|
+
if (!document.startViewTransition) {
|
|
104
|
+
updateFn();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
document.startViewTransition(() => updateFn());
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
/* Only apply transition styles if supported */
|
|
114
|
+
@supports (view-transition-name: none) {
|
|
115
|
+
.hero { view-transition-name: hero; }
|
|
116
|
+
|
|
117
|
+
::view-transition-old(hero) {
|
|
118
|
+
animation: fade-scale-out 250ms ease-out both;
|
|
119
|
+
}
|
|
120
|
+
::view-transition-new(hero) {
|
|
121
|
+
animation: fade-scale-in 300ms ease-in both;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### View Transitions (Cross-Document)
|
|
127
|
+
|
|
128
|
+
```css
|
|
129
|
+
/* This rule is safely ignored by browsers that don't support it */
|
|
130
|
+
@view-transition {
|
|
131
|
+
navigation: auto;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Guard transition-specific styles */
|
|
135
|
+
@supports (view-transition-name: none) {
|
|
136
|
+
.page-header { view-transition-name: header; }
|
|
137
|
+
.main-content { view-transition-name: content; }
|
|
138
|
+
|
|
139
|
+
::view-transition-group(header) {
|
|
140
|
+
animation-duration: 300ms;
|
|
141
|
+
}
|
|
142
|
+
::view-transition-group(content) {
|
|
143
|
+
animation-duration: 250ms;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Strategy**: Pages load normally in unsupported browsers. The `@view-transition` rule is unknown and ignored — no breakage.
|
|
149
|
+
|
|
150
|
+
### @starting-style
|
|
151
|
+
|
|
152
|
+
```css
|
|
153
|
+
/* Broad support — but guard with @supports for very old browsers */
|
|
154
|
+
.popover {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
transform: scale(1);
|
|
157
|
+
transition: opacity 200ms ease, transform 200ms ease;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* @starting-style is baseline — this works in all modern browsers */
|
|
161
|
+
.popover {
|
|
162
|
+
@starting-style {
|
|
163
|
+
opacity: 0;
|
|
164
|
+
transform: scale(0.95);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* For the display transition, check discrete support */
|
|
169
|
+
@supports (transition-behavior: allow-discrete) {
|
|
170
|
+
.popover {
|
|
171
|
+
transition: opacity 200ms ease, transform 200ms ease,
|
|
172
|
+
display 200ms allow-discrete, overlay 200ms allow-discrete;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.popover:not(:popover-open) {
|
|
176
|
+
opacity: 0;
|
|
177
|
+
transform: scale(0.95);
|
|
178
|
+
display: none;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Strategy**: `@starting-style` itself is well-supported. The main concern is `transition-behavior: allow-discrete` for `display`/`overlay` transitions. Guard that part specifically.
|
|
184
|
+
|
|
185
|
+
### Anchor Positioning
|
|
186
|
+
|
|
187
|
+
```css
|
|
188
|
+
/* Feature detection for anchor positioning */
|
|
189
|
+
@supports (anchor-name: --a) {
|
|
190
|
+
.trigger { anchor-name: --trigger; }
|
|
191
|
+
|
|
192
|
+
.tooltip {
|
|
193
|
+
position: fixed;
|
|
194
|
+
position-anchor: --trigger;
|
|
195
|
+
position-area: top center;
|
|
196
|
+
margin-bottom: 0.5rem;
|
|
197
|
+
|
|
198
|
+
position-try-fallbacks: --bottom;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@position-try --bottom {
|
|
202
|
+
position-area: bottom center;
|
|
203
|
+
margin-top: 0.5rem;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Fallback for unsupported browsers: manual absolute positioning */
|
|
208
|
+
@supports not (anchor-name: --a) {
|
|
209
|
+
.tooltip-wrapper {
|
|
210
|
+
position: relative;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.tooltip {
|
|
214
|
+
position: absolute;
|
|
215
|
+
bottom: 100%;
|
|
216
|
+
left: 50%;
|
|
217
|
+
transform: translateX(-50%);
|
|
218
|
+
margin-bottom: 0.5rem;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**Strategy**: Use `@supports (anchor-name: --a)` to detect. Provide a classic `position: absolute` fallback. For JS-heavy apps, Floating UI remains a solid fallback.
|
|
224
|
+
|
|
225
|
+
### Container Queries
|
|
226
|
+
|
|
227
|
+
```css
|
|
228
|
+
/* Container queries are baseline — safe for production */
|
|
229
|
+
.card-wrapper {
|
|
230
|
+
container-type: inline-size;
|
|
231
|
+
container-name: card;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* Size queries: well supported */
|
|
235
|
+
@container card (min-width: 400px) {
|
|
236
|
+
.card { flex-direction: row; }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Style queries: Chrome/Edge/Safari only — use with care */
|
|
240
|
+
@supports (container-type: inline-size) {
|
|
241
|
+
/* Size queries are safe — 95%+ coverage */
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Style queries: progressive enhancement only */
|
|
245
|
+
@container style(--theme: dark) {
|
|
246
|
+
.card { background: oklch(0.2 0.02 250); }
|
|
247
|
+
}
|
|
248
|
+
/* Fallback: use a class or media query for unsupported browsers */
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Progressive Enhancement Strategies
|
|
254
|
+
|
|
255
|
+
### Strategy 1: Layered Animation (Recommended)
|
|
256
|
+
|
|
257
|
+
Build in layers — each layer adds richness but nothing breaks without it.
|
|
258
|
+
|
|
259
|
+
```css
|
|
260
|
+
/* Layer 0: Content is visible, no animation */
|
|
261
|
+
.section-title {
|
|
262
|
+
opacity: 1;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Layer 1: Simple transition (universally supported) */
|
|
266
|
+
.section-title {
|
|
267
|
+
transition: opacity 400ms ease, transform 400ms ease;
|
|
268
|
+
}
|
|
269
|
+
.section-title.visible {
|
|
270
|
+
opacity: 1;
|
|
271
|
+
transform: translateY(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Layer 2: Scroll-driven (modern browsers only) */
|
|
275
|
+
@supports (animation-timeline: scroll()) {
|
|
276
|
+
.section-title {
|
|
277
|
+
animation: reveal linear both;
|
|
278
|
+
animation-timeline: view();
|
|
279
|
+
animation-range: entry 0% entry 80%;
|
|
280
|
+
/* Remove the JS-toggled class approach */
|
|
281
|
+
transition: none;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@keyframes reveal {
|
|
285
|
+
from { opacity: 0; transform: translateY(1.5rem); }
|
|
286
|
+
to { opacity: 1; transform: translateY(0); }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Strategy 2: View Transition as Enhancement
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
// Router integration example (works with any framework)
|
|
295
|
+
async function navigateTo(url) {
|
|
296
|
+
const response = await fetch(url);
|
|
297
|
+
const html = await response.text();
|
|
298
|
+
const parser = new DOMParser();
|
|
299
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
300
|
+
|
|
301
|
+
const updateDOM = () => {
|
|
302
|
+
document.title = doc.title;
|
|
303
|
+
document.querySelector('main').replaceWith(
|
|
304
|
+
doc.querySelector('main')
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (document.startViewTransition) {
|
|
309
|
+
document.startViewTransition(updateDOM);
|
|
310
|
+
} else {
|
|
311
|
+
updateDOM();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
```css
|
|
317
|
+
/* Cross-document: progressive by nature */
|
|
318
|
+
@view-transition { navigation: auto; }
|
|
319
|
+
|
|
320
|
+
/* Reduce motion: respect user preferences */
|
|
321
|
+
@media (prefers-reduced-motion: reduce) {
|
|
322
|
+
::view-transition-group(*),
|
|
323
|
+
::view-transition-old(*),
|
|
324
|
+
::view-transition-new(*) {
|
|
325
|
+
animation-duration: 0.01ms !important;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Strategy 3: @starting-style + Popover API
|
|
331
|
+
|
|
332
|
+
Full native modal with enter/exit animation, zero JavaScript.
|
|
333
|
+
|
|
334
|
+
```html
|
|
335
|
+
<button popovertarget="menu">Open</button>
|
|
336
|
+
|
|
337
|
+
<div id="menu" popover>
|
|
338
|
+
<p>Menu content</p>
|
|
339
|
+
</div>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```css
|
|
343
|
+
[popover] {
|
|
344
|
+
/* Open state */
|
|
345
|
+
opacity: 1;
|
|
346
|
+
transform: translateY(0) scale(1);
|
|
347
|
+
transition: opacity 250ms ease, transform 250ms ease,
|
|
348
|
+
display 250ms allow-discrete, overlay 250ms allow-discrete;
|
|
349
|
+
|
|
350
|
+
/* Entry animation */
|
|
351
|
+
@starting-style {
|
|
352
|
+
opacity: 0;
|
|
353
|
+
transform: translateY(-0.5rem) scale(0.97);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Exit animation */
|
|
358
|
+
[popover]:not(:popover-open) {
|
|
359
|
+
opacity: 0;
|
|
360
|
+
transform: translateY(-0.5rem) scale(0.97);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Strategy 4: Anchor + Popover Combo (Animated Tooltip)
|
|
365
|
+
|
|
366
|
+
```html
|
|
367
|
+
<button anchor="tip" popovertarget="tip">Hover me</button>
|
|
368
|
+
|
|
369
|
+
<div id="tip" popover="hint" anchor="trigger">
|
|
370
|
+
Tooltip content
|
|
371
|
+
</div>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```css
|
|
375
|
+
[popover="hint"] {
|
|
376
|
+
position: fixed;
|
|
377
|
+
position-anchor: --trigger;
|
|
378
|
+
position-area: top center;
|
|
379
|
+
margin-bottom: 0.5rem;
|
|
380
|
+
|
|
381
|
+
/* Animation */
|
|
382
|
+
opacity: 1;
|
|
383
|
+
transform: translateY(0);
|
|
384
|
+
transition: opacity 150ms ease, transform 150ms ease,
|
|
385
|
+
display 150ms allow-discrete, overlay 150ms allow-discrete;
|
|
386
|
+
|
|
387
|
+
@starting-style {
|
|
388
|
+
opacity: 0;
|
|
389
|
+
transform: translateY(4px);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
position-try-fallbacks: --bottom, --left, --right;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@position-try --bottom {
|
|
396
|
+
position-area: bottom center;
|
|
397
|
+
margin-top: 0.5rem;
|
|
398
|
+
}
|
|
399
|
+
@position-try --left {
|
|
400
|
+
position-area: left center;
|
|
401
|
+
margin-right: 0.5rem;
|
|
402
|
+
}
|
|
403
|
+
@position-try --right {
|
|
404
|
+
position-area: right center;
|
|
405
|
+
margin-left: 0.5rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* Respect reduced motion */
|
|
409
|
+
@media (prefers-reduced-motion: reduce) {
|
|
410
|
+
[popover="hint"] {
|
|
411
|
+
transition-duration: 0.01ms;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Accessibility Checklist
|
|
419
|
+
|
|
420
|
+
```css
|
|
421
|
+
/* ALWAYS include this in any project with animations */
|
|
422
|
+
@media (prefers-reduced-motion: reduce) {
|
|
423
|
+
*,
|
|
424
|
+
*::before,
|
|
425
|
+
*::after {
|
|
426
|
+
animation-duration: 0.01ms !important;
|
|
427
|
+
animation-iteration-count: 1 !important;
|
|
428
|
+
transition-duration: 0.01ms !important;
|
|
429
|
+
scroll-behavior: auto !important;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
For scroll-driven animations specifically:
|
|
435
|
+
|
|
436
|
+
```css
|
|
437
|
+
@media (prefers-reduced-motion: reduce) {
|
|
438
|
+
.reveal {
|
|
439
|
+
animation: none;
|
|
440
|
+
opacity: 1;
|
|
441
|
+
transform: none;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Performance Notes
|
|
449
|
+
|
|
450
|
+
| Property | Rendering Cost | GPU Composited |
|
|
451
|
+
|---|---|---|
|
|
452
|
+
| `transform` | Low | Yes |
|
|
453
|
+
| `opacity` | Low | Yes |
|
|
454
|
+
| `filter` / `backdrop-filter` | Medium | Yes |
|
|
455
|
+
| `clip-path` | Medium | Yes (in most browsers) |
|
|
456
|
+
| `background` (gradients) | Medium | No — repaint |
|
|
457
|
+
| `width` / `height` | High | No — reflow + repaint |
|
|
458
|
+
| `top` / `left` / `right` / `bottom` | High | No — reflow + repaint |
|
|
459
|
+
| `box-shadow` | High | No — repaint |
|
|
460
|
+
|
|
461
|
+
Best practices:
|
|
462
|
+
- Use `will-change` sparingly and only on elements about to animate — never `will-change: transform` on 50 elements
|
|
463
|
+
- Prefer `translate`, `scale`, `rotate` individual properties over `transform` shorthand when only one axis changes (allows independent transitions)
|
|
464
|
+
- For scroll-driven animations, the browser automatically optimizes — no `will-change` needed
|
|
465
|
+
- Test with Chrome DevTools > Rendering > "Highlight areas outside the compositing layers" to verify GPU compositing
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev/framer-motion
|
|
3
|
+
description: >
|
|
4
|
+
Framer Motion for React: AnimatePresence exit animations, layout
|
|
5
|
+
animations with stable keys, variant propagation and orchestration,
|
|
6
|
+
gestures, and motion values that update without re-render — plus the
|
|
7
|
+
decision rule for when NOT to use it.
|
|
8
|
+
TRIGGER: "framer motion", "motion for React", "AnimatePresence", "layout
|
|
9
|
+
animation", "anima este componente React", "exit animation", "/dev
|
|
10
|
+
framer-motion".
|
|
11
|
+
SKIP: the same effect achievable in pure CSS (scroll-driven, view
|
|
12
|
+
transitions, entry effects) -> dev/css-native (zero dependency beats a
|
|
13
|
+
library); generative canvas work -> dev/canvas-generative.
|
|
14
|
+
allowed-tools: [Read, Write, Edit, Bash, Grep, Glob]
|
|
15
|
+
metadata:
|
|
16
|
+
origin: community
|
|
17
|
+
source: https://github.com/AThevon/genjutsu
|
|
18
|
+
license: MIT
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<!-- arka:kb-first-prefix begin -->
|
|
22
|
+
> **KB-first:** query `mcp__obsidian__search_notes` (and
|
|
23
|
+
> `mcp__graphify__query_graph` when configured) and cite `[[wikilinks]]`
|
|
24
|
+
> or graph nodes — or declare the gap — BEFORE any external research.
|
|
25
|
+
> Full doctrine: `arka/SKILL.md` (KB-First Research).
|
|
26
|
+
<!-- arka:kb-first-prefix end -->
|
|
27
|
+
|
|
28
|
+
# Framer Motion — `/dev framer-motion`
|
|
29
|
+
|
|
30
|
+
> **Agent:** Diana (Frontend Dev) | **Framework:** Framer Motion (genjutsu)
|
|
31
|
+
|
|
32
|
+
> Package: `motion` (v11+, formerly `framer-motion`). Import: `import { motion, AnimatePresence } from "motion/react"`
|
|
33
|
+
|
|
34
|
+
## When to use Framer Motion vs alternatives
|
|
35
|
+
|
|
36
|
+
| Criteria | Framer Motion | GSAP | Native CSS |
|
|
37
|
+
|---------|--------------|------|-----------|
|
|
38
|
+
| Layout animations | Excellent (layoutId) | Manual | Impossible |
|
|
39
|
+
| Exit animations | AnimatePresence | Timeline reverse | Limited (display) |
|
|
40
|
+
| Gestures (drag, hover) | Native, declarative | Draggable plugin | Basic |
|
|
41
|
+
| Scroll-driven | useScroll + useTransform | ScrollTrigger (more powerful) | scroll-timeline |
|
|
42
|
+
| Complex orchestration | Variants + propagation | Timeline (more flexible) | @keyframes |
|
|
43
|
+
| Bundle size | ~50kb tree-shaken | ~30kb core | 0kb |
|
|
44
|
+
| React integration | Native, component-first | Refs + useGSAP | className toggle |
|
|
45
|
+
|
|
46
|
+
**Rule**: Framer Motion for React UI interactions (modals, toasts, reorder, shared layout). GSAP for complex timelines, cinematic scroll-driven, SVG morphing.
|
|
47
|
+
|
|
48
|
+
## AnimatePresence — Exit animations
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<AnimatePresence mode="wait">
|
|
52
|
+
{isVisible && (
|
|
53
|
+
<motion.div
|
|
54
|
+
key="unique-key" // REQUIRED — identifies the component
|
|
55
|
+
initial={{ opacity: 0 }}
|
|
56
|
+
animate={{ opacity: 1 }}
|
|
57
|
+
exit={{ opacity: 0 }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</AnimatePresence>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `mode="wait"` — waits for exit to finish before enter (page transitions)
|
|
64
|
+
- `mode="sync"` — exit and enter simultaneously
|
|
65
|
+
- `mode="popLayout"` — removes from flow immediately (good for lists)
|
|
66
|
+
- `onExitComplete` — callback when all exit animations are finished
|
|
67
|
+
|
|
68
|
+
## Layout animations
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// Shared layout — the element "slides" between two positions
|
|
72
|
+
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
|
|
73
|
+
|
|
74
|
+
// Auto layout — animates position/size when layout changes
|
|
75
|
+
<motion.div layout>
|
|
76
|
+
{isExpanded && <motion.p layout>Additional content</motion.p>}
|
|
77
|
+
</motion.div>
|
|
78
|
+
|
|
79
|
+
// layout="position" — animates position only (not size)
|
|
80
|
+
// layout="size" — animates size only
|
|
81
|
+
// layout="preserve-aspect" — preserves the ratio during the transition
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Variants — Propagation and orchestration
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
const container = {
|
|
88
|
+
hidden: { opacity: 0 },
|
|
89
|
+
show: {
|
|
90
|
+
opacity: 1,
|
|
91
|
+
transition: {
|
|
92
|
+
staggerChildren: 0.08,
|
|
93
|
+
delayChildren: 0.2,
|
|
94
|
+
staggerDirection: 1, // 1 = normal, -1 = reverse
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const item = {
|
|
100
|
+
hidden: { opacity: 0, y: 20 },
|
|
101
|
+
show: { opacity: 1, y: 0 },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
<motion.ul variants={container} initial="hidden" animate="show">
|
|
105
|
+
{items.map((i) => (
|
|
106
|
+
<motion.li key={i.id} variants={item} />
|
|
107
|
+
))}
|
|
108
|
+
</motion.ul>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Variants **automatically propagate** to motion children — no need for `initial`/`animate` on children.
|
|
112
|
+
|
|
113
|
+
## Gestures
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<motion.div
|
|
117
|
+
whileHover={{ scale: 1.05 }}
|
|
118
|
+
whileTap={{ scale: 0.95 }}
|
|
119
|
+
whileFocus={{ borderColor: "#3b82f6" }}
|
|
120
|
+
// Drag
|
|
121
|
+
drag // true = x+y, "x" = horizontal only, "y" = vertical only
|
|
122
|
+
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
|
|
123
|
+
dragElastic={0.2} // 0 = rigid, 1 = free (default 0.35)
|
|
124
|
+
dragSnapToOrigin // returns to initial position
|
|
125
|
+
onDragEnd={(e, info) => {
|
|
126
|
+
if (info.offset.x > 100) handleSwipe("right");
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Motion values — Reactive without re-render
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const x = useMotionValue(0);
|
|
135
|
+
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
|
|
136
|
+
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
|
|
137
|
+
|
|
138
|
+
// Spring-based smoothing
|
|
139
|
+
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
|
|
140
|
+
|
|
141
|
+
// Scroll tracking
|
|
142
|
+
const { scrollY, scrollYProgress } = useScroll();
|
|
143
|
+
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
|
|
144
|
+
|
|
145
|
+
// Element-scoped scroll
|
|
146
|
+
const ref = useRef(null);
|
|
147
|
+
const { scrollYProgress } = useScroll({
|
|
148
|
+
target: ref,
|
|
149
|
+
offset: ["start end", "end start"],
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Motion values do NOT trigger React re-renders** — they update the DOM directly via `style`.
|
|
154
|
+
|
|
155
|
+
## Do Not
|
|
156
|
+
|
|
157
|
+
### Do not setState in callbacks without a guard
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// BAD — infinite re-render if animate depends on state
|
|
161
|
+
onUpdate={(latest) => setPosition(latest.x)}
|
|
162
|
+
|
|
163
|
+
// GOOD — guard or useMotionValueEvent
|
|
164
|
+
const x = useMotionValue(0);
|
|
165
|
+
useMotionValueEvent(x, "change", (latest) => {
|
|
166
|
+
if (latest > threshold) onThresholdReached();
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Do not use layout animation without a stable key
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
// BAD — key changes every render, breaks layout tracking
|
|
174
|
+
<motion.div layout key={Math.random()} />
|
|
175
|
+
|
|
176
|
+
// GOOD — stable key derived from data
|
|
177
|
+
<motion.div layout key={item.id} />
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Do not forget the unique key on AnimatePresence
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
// BAD — no key, exit animation does not trigger
|
|
184
|
+
<AnimatePresence>
|
|
185
|
+
{isOpen && <motion.div exit={{ opacity: 0 }} />}
|
|
186
|
+
</AnimatePresence>
|
|
187
|
+
|
|
188
|
+
// GOOD — unique key for each conditional child
|
|
189
|
+
<AnimatePresence>
|
|
190
|
+
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
|
|
191
|
+
</AnimatePresence>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Do not wrap an already animated component with motion.div
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// BAD — double animation, transform conflicts
|
|
198
|
+
<motion.div animate={{ x: 100 }}>
|
|
199
|
+
<motion.div animate={{ x: -50 }}>Content</motion.div>
|
|
200
|
+
</motion.div>
|
|
201
|
+
|
|
202
|
+
// GOOD — single animation level per transform axis
|
|
203
|
+
<motion.div animate={{ x: 100 }}>
|
|
204
|
+
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
|
|
205
|
+
</motion.div>
|
|
206
|
+
|
|
207
|
+
// GOOD — use variants to coordinate parent/child
|
|
208
|
+
<motion.div variants={parent} animate="active">
|
|
209
|
+
<motion.div variants={child} />
|
|
210
|
+
</motion.div>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Deep dive
|
|
214
|
+
|
|
215
|
+
| Need | Reference |
|
|
216
|
+
|---|---|
|
|
217
|
+
| Full API reference — motion props (animation, gestures, drag, layout, lifecycle), AnimatePresence modes, transition configuration and ease presets, and the hooks (`useMotionValue`, `useTransform`, `useSpring`, `useScroll`) | `references/api.md` |
|
|
218
|
+
|
|
219
|
+
## Output
|
|
220
|
+
|
|
221
|
+
The motion-wrapped React component(s) with stable keys, variants where
|
|
222
|
+
parent/child coordinate, and gesture guards — plus the reason Framer
|
|
223
|
+
Motion was the right tier for this effect rather than CSS.
|