transitions-refine 0.1.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/skills/refine-live/SKILL.md +205 -0
- package/.agents/skills/transitions-dev/01-card-resize.md +53 -0
- package/.agents/skills/transitions-dev/02-number-pop-in.md +119 -0
- package/.agents/skills/transitions-dev/03-notification-badge.md +110 -0
- package/.agents/skills/transitions-dev/04-text-states-swap.md +97 -0
- package/.agents/skills/transitions-dev/05-menu-dropdown.md +105 -0
- package/.agents/skills/transitions-dev/06-modal.md +94 -0
- package/.agents/skills/transitions-dev/07-panel-reveal.md +81 -0
- package/.agents/skills/transitions-dev/08-page-side-by-side.md +100 -0
- package/.agents/skills/transitions-dev/09-icon-swap.md +78 -0
- package/.agents/skills/transitions-dev/10-success-check.md +169 -0
- package/.agents/skills/transitions-dev/11-avatar-group-hover.md +200 -0
- package/.agents/skills/transitions-dev/12-error-state-shake.md +202 -0
- package/.agents/skills/transitions-dev/13-input-clear-dissolve.md +276 -0
- package/.agents/skills/transitions-dev/14-skeleton-reveal.md +149 -0
- package/.agents/skills/transitions-dev/15-shimmer-text.md +95 -0
- package/.agents/skills/transitions-dev/16-tabs-sliding.md +146 -0
- package/.agents/skills/transitions-dev/17-tooltip.md +103 -0
- package/.agents/skills/transitions-dev/18-texts-reveal.md +110 -0
- package/.agents/skills/transitions-dev/19-card-tilt.md +170 -0
- package/.agents/skills/transitions-dev/20-plus-menu-morph.md +167 -0
- package/.agents/skills/transitions-dev/21-accordion.md +124 -0
- package/.agents/skills/transitions-dev/SKILL.md +225 -0
- package/.agents/skills/transitions-dev/_root.css +204 -0
- package/README.md +89 -0
- package/bin/cli.mjs +264 -0
- package/demo.html +2531 -0
- package/package.json +37 -0
- package/server/inject.mjs +116 -0
- package/server/motion-tokens.mjs +106 -0
- package/server/refine-agent.mjs +86 -0
- package/server/relay.mjs +421 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Modal open / close
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Modal dialogs and full-overlay surfaces that scale up from center. Use when the surface is conceptually "on top of" the page rather than anchored to a trigger.
|
|
6
|
+
|
|
7
|
+
## HTML usage
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="t-modal" role="dialog">…</div>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
State:
|
|
14
|
+
- Add `.is-open` to open (scales up from --modal-scale).
|
|
15
|
+
- On close, swap `.is-open` for `.is-closing`, then remove
|
|
16
|
+
`.is-closing` after --modal-close-dur.
|
|
17
|
+
|
|
18
|
+
## Tunable variables
|
|
19
|
+
|
|
20
|
+
| Variable | Default | Notes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `--modal-open-dur` | `250ms` | sourced from `--p7-open-dur` |
|
|
23
|
+
| `--modal-close-dur` | `150ms` | sourced from `--p7-close-dur` |
|
|
24
|
+
| `--modal-scale` | `0.96` | sourced from `--p7-scale` |
|
|
25
|
+
| `--modal-scale-close` | `0.96` | sourced from `--p7-scale-close` |
|
|
26
|
+
| `--modal-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p7-ease` |
|
|
27
|
+
|
|
28
|
+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
:root {
|
|
32
|
+
--modal-open-dur: 250ms;
|
|
33
|
+
--modal-close-dur: 150ms;
|
|
34
|
+
--modal-scale: 0.96;
|
|
35
|
+
--modal-scale-close: 0.96;
|
|
36
|
+
--modal-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CSS
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
.t-modal {
|
|
44
|
+
transform-origin: center;
|
|
45
|
+
transform: scale(var(--modal-scale));
|
|
46
|
+
opacity: 0;
|
|
47
|
+
pointer-events: none;
|
|
48
|
+
transition:
|
|
49
|
+
transform var(--modal-open-dur) var(--modal-ease),
|
|
50
|
+
opacity var(--modal-open-dur) var(--modal-ease);
|
|
51
|
+
will-change: transform, opacity;
|
|
52
|
+
}
|
|
53
|
+
.t-modal.is-open {
|
|
54
|
+
transform: scale(1);
|
|
55
|
+
opacity: 1;
|
|
56
|
+
pointer-events: auto;
|
|
57
|
+
}
|
|
58
|
+
.t-modal.is-closing {
|
|
59
|
+
transform: scale(var(--modal-scale-close));
|
|
60
|
+
opacity: 0;
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
transition:
|
|
63
|
+
transform var(--modal-close-dur) var(--modal-ease),
|
|
64
|
+
opacity var(--modal-close-dur) var(--modal-ease);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@media (prefers-reduced-motion: reduce) {
|
|
68
|
+
.t-modal { transition: none !important; }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
|
|
73
|
+
|
|
74
|
+
## JavaScript orchestration
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// Same close-then-cleanup pattern as the dropdown — modals scale from
|
|
78
|
+
// --modal-scale up to 1, then on close dip to --modal-scale-close.
|
|
79
|
+
const modal = document.querySelector(".t-modal");
|
|
80
|
+
const closeMs = parseFloat(
|
|
81
|
+
getComputedStyle(document.documentElement).getPropertyValue("--modal-close-dur")
|
|
82
|
+
) || 150;
|
|
83
|
+
|
|
84
|
+
function openModal() {
|
|
85
|
+
modal.classList.remove("is-closing");
|
|
86
|
+
modal.classList.add("is-open");
|
|
87
|
+
}
|
|
88
|
+
function closeModal() {
|
|
89
|
+
modal.classList.remove("is-open");
|
|
90
|
+
modal.classList.add("is-closing");
|
|
91
|
+
setTimeout(() => modal.classList.remove("is-closing"), closeMs);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Panel reveal
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
A panel that slides into view inside an existing container — e.g. detail panel inside a card, expanding section. Combines a short translate, opacity, and a 2px cross-blur so a half-height travel still reads as a full open.
|
|
6
|
+
|
|
7
|
+
## HTML usage
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="t-panel-slide" data-open="false">
|
|
11
|
+
<!-- your panel contents -->
|
|
12
|
+
</div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The panel slides on the Y axis, fades opacity 0 ↔ 1,
|
|
16
|
+
and cross-blurs --panel-blur ↔ 0, all on the same
|
|
17
|
+
duration / ease so a shorter travel (e.g. 50% of the
|
|
18
|
+
panel height) still reads as a full open / close.
|
|
19
|
+
Wrap it in your own container with `overflow: hidden`
|
|
20
|
+
if you want the closed state fully clipped. Set
|
|
21
|
+
--panel-translate-y to the travel distance (e.g. half
|
|
22
|
+
the panel's own height).
|
|
23
|
+
|
|
24
|
+
## Tunable variables
|
|
25
|
+
|
|
26
|
+
| Variable | Default | Notes |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `--panel-open-dur` | `400ms` | sourced from `--p3-open-dur` |
|
|
29
|
+
| `--panel-close-dur` | `350ms` | sourced from `--p3-close-dur` |
|
|
30
|
+
| `--panel-translate-y` | `100px` | sourced from `--p3-translate-y` |
|
|
31
|
+
| `--panel-blur` | `2px` | sourced from `--p3-blur` |
|
|
32
|
+
| `--panel-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p3-ease` |
|
|
33
|
+
|
|
34
|
+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
:root {
|
|
38
|
+
--panel-open-dur: 400ms;
|
|
39
|
+
--panel-close-dur: 350ms;
|
|
40
|
+
--panel-translate-y: 100px;
|
|
41
|
+
--panel-blur: 2px;
|
|
42
|
+
--panel-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## CSS
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
.t-panel-slide {
|
|
50
|
+
transform: translateY(var(--panel-translate-y));
|
|
51
|
+
opacity: 0;
|
|
52
|
+
filter: blur(var(--panel-blur));
|
|
53
|
+
pointer-events: none;
|
|
54
|
+
transition:
|
|
55
|
+
transform var(--panel-close-dur) var(--panel-ease),
|
|
56
|
+
opacity var(--panel-close-dur) var(--panel-ease),
|
|
57
|
+
filter var(--panel-close-dur) var(--panel-ease);
|
|
58
|
+
will-change: transform, opacity, filter;
|
|
59
|
+
}
|
|
60
|
+
.t-panel-slide[data-open="true"] {
|
|
61
|
+
transform: translateY(0);
|
|
62
|
+
opacity: 1;
|
|
63
|
+
filter: blur(0);
|
|
64
|
+
pointer-events: auto;
|
|
65
|
+
transition:
|
|
66
|
+
transform var(--panel-open-dur) var(--panel-ease),
|
|
67
|
+
opacity var(--panel-open-dur) var(--panel-ease),
|
|
68
|
+
filter var(--panel-open-dur) var(--panel-ease);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
.t-panel-slide { transition: none !important; }
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
|
|
77
|
+
|
|
78
|
+
## JavaScript orchestration
|
|
79
|
+
|
|
80
|
+
None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
|
|
81
|
+
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Page side-by-side
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Sliding between two full pages or screens that live side-by-side: list ↔ detail, step 1 ↔ step 2 in a wizard. Page 1 exits left, page 2 exits right.
|
|
6
|
+
|
|
7
|
+
## HTML usage
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="t-page-slide" data-page="1">
|
|
11
|
+
<section class="t-page" data-page-id="1">…</section>
|
|
12
|
+
<section class="t-page" data-page-id="2">…</section>
|
|
13
|
+
</div>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
State: set data-page="1" or "2" on .t-page-slide.
|
|
17
|
+
Page 1 exits to the left, page 2 exits to the right.
|
|
18
|
+
--page-exit-enabled (0/1) disables the outgoing slide.
|
|
19
|
+
|
|
20
|
+
## Tunable variables
|
|
21
|
+
|
|
22
|
+
| Variable | Default | Notes |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `--page-slide-dur` | `250ms` | sourced from `--p8-slide-dur` |
|
|
25
|
+
| `--page-fade-dur` | `250ms` | sourced from `--p8-fade-dur` |
|
|
26
|
+
| `--page-slide-distance` | `8px` | sourced from `--p8-distance` |
|
|
27
|
+
| `--page-blur` | `3px` | sourced from `--p8-blur` |
|
|
28
|
+
| `--page-stagger` | `0ms` | sourced from `--p8-stagger` |
|
|
29
|
+
| `--page-exit-enabled` | `1` | sourced from `--p8-exit-enabled` |
|
|
30
|
+
| `--page-slide-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p8-slide-ease` |
|
|
31
|
+
| `--page-fade-ease` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p8-fade-ease` |
|
|
32
|
+
|
|
33
|
+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
:root {
|
|
37
|
+
--page-slide-dur: 250ms;
|
|
38
|
+
--page-fade-dur: 250ms;
|
|
39
|
+
--page-slide-distance: 8px;
|
|
40
|
+
--page-blur: 3px;
|
|
41
|
+
--page-stagger: 0ms;
|
|
42
|
+
--page-exit-enabled: 1;
|
|
43
|
+
--page-slide-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
44
|
+
--page-fade-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## CSS
|
|
49
|
+
|
|
50
|
+
```css
|
|
51
|
+
.t-page-slide {
|
|
52
|
+
position: relative;
|
|
53
|
+
}
|
|
54
|
+
.t-page-slide .t-page[data-page-id="1"] {
|
|
55
|
+
--t-page-from-x: calc(var(--page-slide-distance) * -1);
|
|
56
|
+
}
|
|
57
|
+
.t-page-slide .t-page[data-page-id="2"] {
|
|
58
|
+
--t-page-from-x: var(--page-slide-distance);
|
|
59
|
+
}
|
|
60
|
+
.t-page-slide .t-page {
|
|
61
|
+
position: absolute;
|
|
62
|
+
inset: 0;
|
|
63
|
+
opacity: 0;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
transform: translateX(calc(var(--t-page-from-x, 0px) * var(--page-exit-enabled)));
|
|
66
|
+
filter: blur(calc(var(--page-blur) * var(--page-exit-enabled)));
|
|
67
|
+
transition:
|
|
68
|
+
opacity var(--page-fade-dur) var(--page-fade-ease),
|
|
69
|
+
transform var(--page-slide-dur) var(--page-slide-ease),
|
|
70
|
+
filter var(--page-slide-dur) var(--page-slide-ease);
|
|
71
|
+
will-change: opacity, transform, filter;
|
|
72
|
+
}
|
|
73
|
+
.t-page-slide[data-page="1"] .t-page[data-page-id="1"],
|
|
74
|
+
.t-page-slide[data-page="2"] .t-page[data-page-id="2"] {
|
|
75
|
+
opacity: 1;
|
|
76
|
+
pointer-events: auto;
|
|
77
|
+
transform: translateX(0);
|
|
78
|
+
filter: blur(0);
|
|
79
|
+
transition-delay: var(--page-stagger);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@media (prefers-reduced-motion: reduce) {
|
|
83
|
+
.t-page-slide .t-page { transition: none !important; }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
|
|
88
|
+
|
|
89
|
+
## JavaScript orchestration
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
// Flip data-page on the container — the CSS handles the rest.
|
|
93
|
+
// Set --page-exit-enabled: 0 on the container if you want pages to
|
|
94
|
+
// fade without sliding (useful on first paint).
|
|
95
|
+
const slider = document.querySelector(".t-page-slide");
|
|
96
|
+
function showPage(n) {
|
|
97
|
+
slider.setAttribute("data-page", String(n));
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Icon swap
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Cross-fading two icons in the same slot — hamburger ↔ close, sun ↔ moon, play ↔ pause, expand ↔ collapse. Both icons stay in the DOM stacked in the same grid cell.
|
|
6
|
+
|
|
7
|
+
## HTML usage
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div class="t-icon-swap" data-state="a">
|
|
11
|
+
<span class="t-icon" data-icon="a">…</span>
|
|
12
|
+
<span class="t-icon" data-icon="b">…</span>
|
|
13
|
+
</div>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
State: set data-state="a" or "b" on .t-icon-swap.
|
|
17
|
+
The matching .t-icon fades in; the other fades out with blur
|
|
18
|
+
and scale.
|
|
19
|
+
|
|
20
|
+
## Tunable variables
|
|
21
|
+
|
|
22
|
+
| Variable | Default | Notes |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `--icon-swap-dur` | `250ms` | sourced from `--p5-dur` |
|
|
25
|
+
| `--icon-swap-blur` | `2px` | sourced from `--p5-blur` |
|
|
26
|
+
| `--icon-swap-start-scale` | `0.25` | sourced from `--p5-start-scale` |
|
|
27
|
+
| `--icon-swap-ease` | `ease-in-out` | sourced from `--p5-ease` |
|
|
28
|
+
|
|
29
|
+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
|
|
30
|
+
|
|
31
|
+
```css
|
|
32
|
+
:root {
|
|
33
|
+
--icon-swap-dur: 250ms;
|
|
34
|
+
--icon-swap-blur: 2px;
|
|
35
|
+
--icon-swap-start-scale: 0.25;
|
|
36
|
+
--icon-swap-ease: ease-in-out;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CSS
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
.t-icon-swap {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: inline-grid;
|
|
46
|
+
}
|
|
47
|
+
.t-icon-swap .t-icon {
|
|
48
|
+
grid-area: 1 / 1;
|
|
49
|
+
transition:
|
|
50
|
+
opacity var(--icon-swap-dur) var(--icon-swap-ease),
|
|
51
|
+
filter var(--icon-swap-dur) var(--icon-swap-ease),
|
|
52
|
+
transform var(--icon-swap-dur) var(--icon-swap-ease);
|
|
53
|
+
will-change: opacity, filter, transform;
|
|
54
|
+
}
|
|
55
|
+
.t-icon-swap[data-state="a"] .t-icon[data-icon="a"],
|
|
56
|
+
.t-icon-swap[data-state="b"] .t-icon[data-icon="b"] {
|
|
57
|
+
opacity: 1;
|
|
58
|
+
filter: blur(0);
|
|
59
|
+
transform: scale(1);
|
|
60
|
+
}
|
|
61
|
+
.t-icon-swap[data-state="a"] .t-icon[data-icon="b"],
|
|
62
|
+
.t-icon-swap[data-state="b"] .t-icon[data-icon="a"] {
|
|
63
|
+
opacity: 0;
|
|
64
|
+
filter: blur(var(--icon-swap-blur));
|
|
65
|
+
transform: scale(var(--icon-swap-start-scale));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (prefers-reduced-motion: reduce) {
|
|
69
|
+
.t-icon-swap .t-icon { transition: none !important; }
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
|
|
74
|
+
|
|
75
|
+
## JavaScript orchestration
|
|
76
|
+
|
|
77
|
+
None — pure CSS. Toggle the documented HTML attributes or class names from whatever already drives state in your app.
|
|
78
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Success check
|
|
2
|
+
|
|
3
|
+
## When to use
|
|
4
|
+
|
|
5
|
+
Confirming a completed action — payment processed, file uploaded, message sent, form saved. The icon fades in, rotates upright, settles with a Y-bob, and (for SVG icons) draws its path stroke. Use whenever a status changes from "pending / unknown" to "success" and you want the moment to feel earned rather than instantaneous.
|
|
6
|
+
|
|
7
|
+
The snippet covers the **appear transition only** — bring your own hide behavior (e.g. unmount, opacity:0, or a custom exit). This is intentional: success states are usually persistent, and a soft fade-out is rarely worth the extra DOM/JS surface.
|
|
8
|
+
|
|
9
|
+
## HTML usage
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<!-- Wrap your icon (SVG, image, anything) in .t-success-check.
|
|
13
|
+
The wrapper drives fade + rotate + blur + Y-bob; if your
|
|
14
|
+
icon is an SVG <path>, it gets the stroke-draw animation.
|
|
15
|
+
Bring your own icon size / colors. -->
|
|
16
|
+
<span class="t-success-check" data-state="out" aria-hidden="true">
|
|
17
|
+
<svg viewBox="0 0 48 48" fill="none">
|
|
18
|
+
<!-- your icon path(s) here -->
|
|
19
|
+
</svg>
|
|
20
|
+
</span>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Trigger:
|
|
24
|
+
- Cold load is data-state="out" (opacity 0; no animation).
|
|
25
|
+
- Show: set data-state="in" (fade + rotate + blur + Y-bob
|
|
26
|
+
+ path draw run in parallel).
|
|
27
|
+
|
|
28
|
+
Snippet covers the appear transition only — bring your own
|
|
29
|
+
hide behavior (e.g. unmount, opacity:0, or a custom exit).
|
|
30
|
+
|
|
31
|
+
## Tunable variables
|
|
32
|
+
|
|
33
|
+
| Variable | Default | Notes |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `--check-opacity-dur` | `500ms` | sourced from `--p10-opacity-dur` |
|
|
36
|
+
| `--check-rotate-dur` | `500ms` | sourced from `--p10-rotate-dur` |
|
|
37
|
+
| `--check-rotate-from` | `80deg` | sourced from `--p10-rotate-from` |
|
|
38
|
+
| `--check-bob-dur` | `500ms` | sourced from `--p10-bob-dur` |
|
|
39
|
+
| `--check-y-amount` | `40px` | sourced from `--p10-y-amount` |
|
|
40
|
+
| `--check-blur-dur` | `500ms` | sourced from `--p10-blur-dur` |
|
|
41
|
+
| `--check-blur-from` | `10px` | sourced from `--p10-blur-from` |
|
|
42
|
+
| `--check-path-dur` | `500ms` | sourced from `--p10-path-dur` |
|
|
43
|
+
| `--check-path-delay` | `80ms` | sourced from `--p10-path-delay` |
|
|
44
|
+
| `--check-ease-out` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-out` |
|
|
45
|
+
| `--check-ease-opacity` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-opacity` |
|
|
46
|
+
| `--check-ease-rotate` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-rotate` |
|
|
47
|
+
| `--check-ease-bob` | `cubic-bezier(0.34, 1.35, 0.64, 1)` | sourced from `--p10-ease-bob` |
|
|
48
|
+
| `--check-ease-path` | `cubic-bezier(0.22, 1, 0.36, 1)` | sourced from `--p10-ease-path` |
|
|
49
|
+
|
|
50
|
+
The `:root` defaults below match the live tuning on [transitions.dev](https://transitions.dev). Drop them into your global stylesheet once — every transition in this skill reads from semantic names like these, so multiple transitions can share a single `:root` block.
|
|
51
|
+
|
|
52
|
+
```css
|
|
53
|
+
:root {
|
|
54
|
+
--check-opacity-dur: 500ms;
|
|
55
|
+
--check-rotate-dur: 500ms;
|
|
56
|
+
--check-rotate-from: 80deg;
|
|
57
|
+
--check-bob-dur: 500ms;
|
|
58
|
+
--check-y-amount: 40px;
|
|
59
|
+
--check-blur-dur: 500ms;
|
|
60
|
+
--check-blur-from: 10px;
|
|
61
|
+
--check-path-dur: 500ms;
|
|
62
|
+
--check-path-delay: 80ms;
|
|
63
|
+
--check-ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
|
64
|
+
--check-ease-opacity: cubic-bezier(0.22, 1, 0.36, 1);
|
|
65
|
+
--check-ease-rotate: cubic-bezier(0.22, 1, 0.36, 1);
|
|
66
|
+
--check-ease-bob: cubic-bezier(0.34, 1.35, 0.64, 1);
|
|
67
|
+
--check-ease-path: cubic-bezier(0.22, 1, 0.36, 1);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## CSS
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
/* Wrapper drives the appear animation; it doesn't own any
|
|
75
|
+
sizing or color so you can drop in any icon. */
|
|
76
|
+
.t-success-check {
|
|
77
|
+
display: inline-block;
|
|
78
|
+
transform-origin: center;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
will-change: transform, opacity, filter;
|
|
81
|
+
}
|
|
82
|
+
/* overflow: visible keeps the stroke from clipping while it
|
|
83
|
+
draws; display: block kills the inline whitespace under SVGs. */
|
|
84
|
+
.t-success-check svg { display: block; overflow: visible; }
|
|
85
|
+
/* Stroke-draw setup. Replace 20 with the result of
|
|
86
|
+
path.getTotalLength() for your path; round caps mean any
|
|
87
|
+
sub-pixel overshoot is invisible. */
|
|
88
|
+
.t-success-check svg path {
|
|
89
|
+
stroke-dasharray: 20;
|
|
90
|
+
stroke-dashoffset: 20;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.t-success-check[data-state="in"] {
|
|
94
|
+
animation:
|
|
95
|
+
t-check-fade var(--check-opacity-dur) var(--check-ease-opacity) forwards,
|
|
96
|
+
t-check-rotate var(--check-rotate-dur) var(--check-ease-rotate) forwards,
|
|
97
|
+
t-check-blur var(--check-blur-dur) var(--check-ease-out) forwards,
|
|
98
|
+
t-check-bob var(--check-bob-dur) var(--check-ease-bob) forwards;
|
|
99
|
+
}
|
|
100
|
+
.t-success-check[data-state="in"] svg path {
|
|
101
|
+
animation: t-check-draw var(--check-path-dur) var(--check-ease-path) var(--check-path-delay, 0ms) forwards;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@keyframes t-check-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
105
|
+
@keyframes t-check-rotate {
|
|
106
|
+
from { transform: rotate(var(--check-rotate-from)); }
|
|
107
|
+
to { transform: rotate(0deg); }
|
|
108
|
+
}
|
|
109
|
+
@keyframes t-check-blur {
|
|
110
|
+
from { filter: blur(var(--check-blur-from)); }
|
|
111
|
+
to { filter: blur(0); }
|
|
112
|
+
}
|
|
113
|
+
@keyframes t-check-bob {
|
|
114
|
+
from { translate: 0 var(--check-y-amount); }
|
|
115
|
+
to { translate: 0 0; }
|
|
116
|
+
}
|
|
117
|
+
@keyframes t-check-draw { to { stroke-dashoffset: 0; } }
|
|
118
|
+
|
|
119
|
+
@media (prefers-reduced-motion: reduce) {
|
|
120
|
+
.t-success-check { animation: none !important; opacity: 1; }
|
|
121
|
+
.t-success-check svg path { animation: none !important; stroke-dashoffset: 0 !important; }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `@media (prefers-reduced-motion: reduce)` guard at the bottom of the snippet is required — keep it. It zeroes the transition for users who have asked for less motion at the OS level.
|
|
126
|
+
|
|
127
|
+
## JavaScript orchestration
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// Cold-load → "out" (no animation). On show, flip to "in".
|
|
131
|
+
// Replay-on-retrigger: reset to "out", force a reflow, then flip
|
|
132
|
+
// back to "in" so the keyframes restart from offset 0.
|
|
133
|
+
const check = document.querySelector(".t-success-check");
|
|
134
|
+
|
|
135
|
+
function showCheck() {
|
|
136
|
+
check.setAttribute("data-state", "out");
|
|
137
|
+
void check.offsetWidth; // force reflow so keyframes restart
|
|
138
|
+
check.setAttribute("data-state", "in");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// If the icon is mounted unconditionally and only shown after some
|
|
142
|
+
// event (e.g. await save()), the simpler form is enough:
|
|
143
|
+
// check.setAttribute("data-state", "in");
|
|
144
|
+
// The reflow trick only matters when you replay the appear from
|
|
145
|
+
// an already-visible state.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Calibrating `stroke-dasharray` for your path
|
|
149
|
+
|
|
150
|
+
The CSS hardcodes `stroke-dasharray: 20` as a placeholder. For a clean draw, replace 20 with the actual length of **your** path (in user units), measured once with `path.getTotalLength()`. Two ways to do it:
|
|
151
|
+
|
|
152
|
+
1. **Static (recommended)** — measure the path in the browser console once, then paste the rounded-up integer into the CSS:
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
document.querySelector(".t-success-check svg path").getTotalLength()
|
|
156
|
+
// → 19.42 → use stroke-dasharray: 20 (round up by 1px for safety)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
2. **Dynamic** — measure on mount and set both properties inline. Use this when paths vary per-render:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
const path = wrapper.querySelector("svg path");
|
|
163
|
+
const len = Math.ceil(path.getTotalLength());
|
|
164
|
+
path.style.strokeDasharray = String(len);
|
|
165
|
+
path.style.strokeDashoffset = String(len);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If the dasharray is too short the stroke pre-reveals before the animation starts; too long and the path appears to draw past its end before fading in. Round up by 1px to absorb sub-pixel float jitter.
|
|
169
|
+
|