From e01dae4e0229ccae8793a96eb7622fca7a35aab0 Mon Sep 17 00:00:00 2001
From: Pete Petrash
+ By one of the creators of
+
+ tw-shimmer
+ CSS-only shimmer effects, with 475k+ weekly npm
+ downloads.
+ .
- hey! finally trying tw-fade 👋 + this feed has a hard edge
- oh nice — welcome + add fade-y to the scroll area
- these scroll lists are slick + do i need another wrapper?
- thanks, it’s all CSS + no. use the element that scrolls
- this thread fades at the edges - while i scroll 👀 + at the top, does the fade stay?
- that’s tw-fade ✨ + no, the top fade clears
- some scroll library? + and at the end?
- nope — a pure CSS mask + the bottom fade clears
- zero JavaScript + the newest message stays clear
- so how does it know i’m - scrolling? + horizontal tabs too?
- scroll-driven CSS — the fade is - scroll-gated + use fade-x on the rail
- does it soften both edges? + can i make the fade wider?
- fade-y — one class on the - container + set a larger range
- but the newest message stays - sharp 🔥 + top edge by itself?
- pinned to the bottom, yep — scroll - up and both edges dissolve ↕︎ + use fade-t
- set a larger range + set a larger reveal
- use fade-t + use fade-top
fade-xyy
fade-range-fade-reveal-md2xl
@@ -1254,7 +1254,7 @@
- set a larger reveal + set a larger ramp
fade-reveal-fade-ramp-
-
-
+
+ Direction
+
+
+
+
+
+
+
- Axis, size, reveal, and clear-zone utilities compose + Axis, size, ramp, and clear-zone utilities compose directly on the scroll container.
@@ -2268,7 +2302,7 @@ />- set a larger ramp + set a larger size
fade-ramp-fade-travel-
@@ -2079,7 +2079,7 @@
x="91"
y="118"
>
- ramp-2xl
+ travel-2xl
- Axis, size, ramp, and clear-zone utilities compose
+ Axis, size, travel, and clear-zone utilities compose
directly on the scroll container.
@@ -2302,7 +2302,7 @@
/>
Partial support in stable Firefox: the mask
- renders, but the ramp falls back to a static
+ renders, but the travel falls back to a static
always-on fade.
diff --git a/demo/styles.css b/demo/styles.css
index b2b33a4..a8c3beb 100644
--- a/demo/styles.css
+++ b/demo/styles.css
@@ -35,13 +35,13 @@
--fade-size-2xl: calc(var(--spacing, 0.25rem) * 24);
--fade-size-3xl: calc(var(--spacing, 0.25rem) * 32);
--fade-size-4xl: calc(var(--spacing, 0.25rem) * 40);
- --fade-ramp-sm: calc(var(--spacing, 0.25rem) * 8);
- --fade-ramp-md: calc(var(--spacing, 0.25rem) * 12);
- --fade-ramp-lg: calc(var(--spacing, 0.25rem) * 16);
- --fade-ramp-xl: calc(var(--spacing, 0.25rem) * 20);
- --fade-ramp-2xl: calc(var(--spacing, 0.25rem) * 24);
- --fade-ramp-3xl: calc(var(--spacing, 0.25rem) * 32);
- --fade-ramp-4xl: calc(var(--spacing, 0.25rem) * 40);
+ --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8);
+ --fade-travel-md: calc(var(--spacing, 0.25rem) * 12);
+ --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16);
+ --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20);
+ --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24);
+ --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32);
+ --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40);
}
}
@layer base {
@@ -532,27 +532,6 @@
-webkit-user-select: none;
user-select: none;
}
- .fade-ramp-2xl {
- --tw-fade-ramp: var(--fade-ramp-2xl);
- }
- .fade-ramp-3xl {
- --tw-fade-ramp: var(--fade-ramp-3xl);
- }
- .fade-ramp-4xl {
- --tw-fade-ramp: var(--fade-ramp-4xl);
- }
- .fade-ramp-lg {
- --tw-fade-ramp: var(--fade-ramp-lg);
- }
- .fade-ramp-md {
- --tw-fade-ramp: var(--fade-ramp-md);
- }
- .fade-ramp-sm {
- --tw-fade-ramp: var(--fade-ramp-sm);
- }
- .fade-ramp-xl {
- --tw-fade-ramp: var(--fade-ramp-xl);
- }
.fade-size-2xl {
--tw-fade-size: var(--fade-size-2xl);
}
@@ -574,6 +553,27 @@
.fade-size-xl {
--tw-fade-size: var(--fade-size-xl);
}
+ .fade-travel-2xl {
+ --tw-fade-travel: var(--fade-travel-2xl);
+ }
+ .fade-travel-3xl {
+ --tw-fade-travel: var(--fade-travel-3xl);
+ }
+ .fade-travel-4xl {
+ --tw-fade-travel: var(--fade-travel-4xl);
+ }
+ .fade-travel-lg {
+ --tw-fade-travel: var(--fade-travel-lg);
+ }
+ .fade-travel-md {
+ --tw-fade-travel: var(--fade-travel-md);
+ }
+ .fade-travel-sm {
+ --tw-fade-travel: var(--fade-travel-sm);
+ }
+ .fade-travel-xl {
+ --tw-fade-travel: var(--fade-travel-xl);
+ }
.sm\:mr-4 {
@media (width >= 40rem) {
margin-right: calc(var(--spacing) * 4);
@@ -684,7 +684,7 @@
syntax: "*";
inherits: false;
}
-@property --tw-fade-ramp {
+@property --tw-fade-travel {
syntax: "*";
inherits: false;
}
@@ -796,7 +796,7 @@
syntax: "*";
inherits: false;
}
-@keyframes tw-fade-ramp-t {
+@keyframes tw-fade-travel-t {
from {
--tw-fade-t: 0;
}
@@ -804,7 +804,7 @@
--tw-fade-t: 1;
}
}
-@keyframes tw-fade-ramp-b {
+@keyframes tw-fade-travel-b {
from {
--tw-fade-b: 1;
}
@@ -812,7 +812,7 @@
--tw-fade-b: 0;
}
}
-@keyframes tw-fade-ramp-l {
+@keyframes tw-fade-travel-l {
from {
--tw-fade-l: 0;
}
@@ -820,7 +820,7 @@
--tw-fade-l: 1;
}
}
-@keyframes tw-fade-ramp-r {
+@keyframes tw-fade-travel-r {
from {
--tw-fade-r: 1;
}
@@ -828,7 +828,7 @@
--tw-fade-r: 0;
}
}
-@keyframes tw-fade-ramp-r-leading {
+@keyframes tw-fade-travel-r-leading {
from {
--tw-fade-r: 0;
}
@@ -836,7 +836,7 @@
--tw-fade-r: 1;
}
}
-@keyframes tw-fade-ramp-l-trailing {
+@keyframes tw-fade-travel-l-trailing {
from {
--tw-fade-l: 1;
}
@@ -847,7 +847,7 @@
@layer utilities {
.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end {
--tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)));
- --tw-fade-ramp-active: var(--tw-fade-ramp, var(--fade-ramp-md, calc(var(--spacing, 0.25rem) * 12)));
+ --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8)));
--tw-fade-identity-mask: linear-gradient(#000, #000);
--tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
--tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
@@ -861,115 +861,120 @@
--tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b));
--tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l));
--tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r));
+ --tw-fade-onset: 8;
+ --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset)));
+ --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset)));
+ --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset)));
+ --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset)));
--tw-fade-gradient-t: linear-gradient(
to bottom,
#000 0 var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t))
);
--tw-fade-gradient-b: linear-gradient(
to top,
#000 0 var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b))
);
--tw-fade-gradient-l: linear-gradient(
to right,
#000 0 var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l))
);
--tw-fade-gradient-r: linear-gradient(
to left,
#000 0 var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r))
);
@@ -1008,35 +1013,35 @@
}
.fade, .fade-y, .fade-top {
--tw-fade-mask-t: var(--tw-fade-gradient-t);
- --tw-fade-animation-t: tw-fade-ramp-t;
+ --tw-fade-animation-t: tw-fade-travel-t;
--tw-fade-timeline-t: scroll(self y);
- --tw-fade-animation-range-t: 0 var(--tw-fade-ramp-active);
+ --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active);
}
.fade, .fade-y, .fade-bottom {
--tw-fade-mask-b: var(--tw-fade-gradient-b);
- --tw-fade-animation-b: tw-fade-ramp-b;
+ --tw-fade-animation-b: tw-fade-travel-b;
--tw-fade-timeline-b: scroll(self y);
- --tw-fade-animation-range-b: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%;
}
.fade, .fade-x, .fade-start {
--tw-fade-ltr-start-layer: var(--tw-fade-gradient-l);
--tw-fade-rtl-start-layer: var(--tw-fade-gradient-r);
- --tw-fade-ltr-start-animation: tw-fade-ramp-l;
- --tw-fade-rtl-start-animation: tw-fade-ramp-r-leading;
+ --tw-fade-ltr-start-animation: tw-fade-travel-l;
+ --tw-fade-rtl-start-animation: tw-fade-travel-r-leading;
--tw-fade-ltr-start-timeline: scroll(self inline);
--tw-fade-rtl-start-timeline: scroll(self inline);
- --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-ramp-active);
- --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-ramp-active);
+ --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active);
+ --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active);
}
.fade, .fade-x, .fade-end {
--tw-fade-ltr-end-layer: var(--tw-fade-gradient-r);
--tw-fade-rtl-end-layer: var(--tw-fade-gradient-l);
- --tw-fade-ltr-end-animation: tw-fade-ramp-r;
- --tw-fade-rtl-end-animation: tw-fade-ramp-l-trailing;
+ --tw-fade-ltr-end-animation: tw-fade-travel-r;
+ --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing;
--tw-fade-ltr-end-timeline: scroll(self inline);
--tw-fade-rtl-end-timeline: scroll(self inline);
- --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
- --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
+ --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
}
@supports (animation-timeline: scroll()) {
.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end {
@@ -1309,7 +1314,7 @@
--tw-fade-size-bottom: initial;
--tw-fade-size-start: initial;
--tw-fade-size-end: initial;
- --tw-fade-ramp: initial;
+ --tw-fade-travel: initial;
--tw-fade-clear: initial;
--tw-fade-clear-y: initial;
--tw-fade-clear-x: initial;
diff --git a/dist/tw-fade.css b/dist/tw-fade.css
index bab9f8e..4bf11f5 100644
--- a/dist/tw-fade.css
+++ b/dist/tw-fade.css
@@ -190,30 +190,6 @@
.fade-clear-y-xs {
--tw-fade-clear-y: var(--fade-clear-xs);
}
-.fade-ramp-2xl {
- --tw-fade-ramp: var(--fade-ramp-2xl);
-}
-.fade-ramp-3xl {
- --tw-fade-ramp: var(--fade-ramp-3xl);
-}
-.fade-ramp-4xl {
- --tw-fade-ramp: var(--fade-ramp-4xl);
-}
-.fade-ramp-lg {
- --tw-fade-ramp: var(--fade-ramp-lg);
-}
-.fade-ramp-md {
- --tw-fade-ramp: var(--fade-ramp-md);
-}
-.fade-ramp-sm {
- --tw-fade-ramp: var(--fade-ramp-sm);
-}
-.fade-ramp-xl {
- --tw-fade-ramp: var(--fade-ramp-xl);
-}
-.fade-ramp-xs {
- --tw-fade-ramp: var(--fade-ramp-xs);
-}
.fade-size-2xl {
--tw-fade-size: var(--fade-size-2xl);
}
@@ -382,6 +358,30 @@
.fade-size-y-xs {
--tw-fade-size-y: var(--fade-size-xs);
}
+.fade-travel-2xl {
+ --tw-fade-travel: var(--fade-travel-2xl);
+}
+.fade-travel-3xl {
+ --tw-fade-travel: var(--fade-travel-3xl);
+}
+.fade-travel-4xl {
+ --tw-fade-travel: var(--fade-travel-4xl);
+}
+.fade-travel-lg {
+ --tw-fade-travel: var(--fade-travel-lg);
+}
+.fade-travel-md {
+ --tw-fade-travel: var(--fade-travel-md);
+}
+.fade-travel-sm {
+ --tw-fade-travel: var(--fade-travel-sm);
+}
+.fade-travel-xl {
+ --tw-fade-travel: var(--fade-travel-xl);
+}
+.fade-travel-xs {
+ --tw-fade-travel: var(--fade-travel-xs);
+}
@property --tw-fade-t {
syntax: "";
inherits: false;
@@ -430,7 +430,7 @@
syntax: "*";
inherits: false;
}
-@property --tw-fade-ramp {
+@property --tw-fade-travel {
syntax: "*";
inherits: false;
}
@@ -551,14 +551,14 @@
--fade-size-2xl: calc(var(--spacing, 0.25rem) * 24);
--fade-size-3xl: calc(var(--spacing, 0.25rem) * 32);
--fade-size-4xl: calc(var(--spacing, 0.25rem) * 40);
- --fade-ramp-xs: calc(var(--spacing, 0.25rem) * 6);
- --fade-ramp-sm: calc(var(--spacing, 0.25rem) * 8);
- --fade-ramp-md: calc(var(--spacing, 0.25rem) * 12);
- --fade-ramp-lg: calc(var(--spacing, 0.25rem) * 16);
- --fade-ramp-xl: calc(var(--spacing, 0.25rem) * 20);
- --fade-ramp-2xl: calc(var(--spacing, 0.25rem) * 24);
- --fade-ramp-3xl: calc(var(--spacing, 0.25rem) * 32);
- --fade-ramp-4xl: calc(var(--spacing, 0.25rem) * 40);
+ --fade-travel-xs: calc(var(--spacing, 0.25rem) * 6);
+ --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8);
+ --fade-travel-md: calc(var(--spacing, 0.25rem) * 12);
+ --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16);
+ --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20);
+ --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24);
+ --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32);
+ --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40);
--fade-clear-xs: calc(var(--spacing, 0.25rem) * 6);
--fade-clear-sm: calc(var(--spacing, 0.25rem) * 8);
--fade-clear-md: calc(var(--spacing, 0.25rem) * 12);
@@ -568,7 +568,7 @@
--fade-clear-3xl: calc(var(--spacing, 0.25rem) * 32);
--fade-clear-4xl: calc(var(--spacing, 0.25rem) * 40);
}
-@keyframes tw-fade-ramp-t {
+@keyframes tw-fade-travel-t {
from {
--tw-fade-t: 0;
}
@@ -576,7 +576,7 @@
--tw-fade-t: 1;
}
}
-@keyframes tw-fade-ramp-b {
+@keyframes tw-fade-travel-b {
from {
--tw-fade-b: 1;
}
@@ -584,7 +584,7 @@
--tw-fade-b: 0;
}
}
-@keyframes tw-fade-ramp-l {
+@keyframes tw-fade-travel-l {
from {
--tw-fade-l: 0;
}
@@ -592,7 +592,7 @@
--tw-fade-l: 1;
}
}
-@keyframes tw-fade-ramp-r {
+@keyframes tw-fade-travel-r {
from {
--tw-fade-r: 1;
}
@@ -600,7 +600,7 @@
--tw-fade-r: 0;
}
}
-@keyframes tw-fade-ramp-r-leading {
+@keyframes tw-fade-travel-r-leading {
from {
--tw-fade-r: 0;
}
@@ -608,7 +608,7 @@
--tw-fade-r: 1;
}
}
-@keyframes tw-fade-ramp-l-trailing {
+@keyframes tw-fade-travel-l-trailing {
from {
--tw-fade-l: 1;
}
@@ -619,7 +619,7 @@
@layer utilities {
.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end {
--tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)));
- --tw-fade-ramp-active: var(--tw-fade-ramp, var(--fade-ramp-md, calc(var(--spacing, 0.25rem) * 12)));
+ --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8)));
--tw-fade-identity-mask: linear-gradient(#000, #000);
--tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
--tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
@@ -633,115 +633,120 @@
--tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b));
--tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l));
--tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r));
+ --tw-fade-onset: 8;
+ --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset)));
+ --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset)));
+ --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset)));
+ --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset)));
--tw-fade-gradient-t: linear-gradient(
to bottom,
#000 0 var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t))
);
--tw-fade-gradient-b: linear-gradient(
to top,
#000 0 var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b))
);
--tw-fade-gradient-l: linear-gradient(
to right,
#000 0 var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l))
);
--tw-fade-gradient-r: linear-gradient(
to left,
#000 0 var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r))
);
@@ -780,35 +785,35 @@
}
.fade, .fade-y, .fade-top {
--tw-fade-mask-t: var(--tw-fade-gradient-t);
- --tw-fade-animation-t: tw-fade-ramp-t;
+ --tw-fade-animation-t: tw-fade-travel-t;
--tw-fade-timeline-t: scroll(self y);
- --tw-fade-animation-range-t: 0 var(--tw-fade-ramp-active);
+ --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active);
}
.fade, .fade-y, .fade-bottom {
--tw-fade-mask-b: var(--tw-fade-gradient-b);
- --tw-fade-animation-b: tw-fade-ramp-b;
+ --tw-fade-animation-b: tw-fade-travel-b;
--tw-fade-timeline-b: scroll(self y);
- --tw-fade-animation-range-b: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%;
}
.fade, .fade-x, .fade-start {
--tw-fade-ltr-start-layer: var(--tw-fade-gradient-l);
--tw-fade-rtl-start-layer: var(--tw-fade-gradient-r);
- --tw-fade-ltr-start-animation: tw-fade-ramp-l;
- --tw-fade-rtl-start-animation: tw-fade-ramp-r-leading;
+ --tw-fade-ltr-start-animation: tw-fade-travel-l;
+ --tw-fade-rtl-start-animation: tw-fade-travel-r-leading;
--tw-fade-ltr-start-timeline: scroll(self inline);
--tw-fade-rtl-start-timeline: scroll(self inline);
- --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-ramp-active);
- --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-ramp-active);
+ --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active);
+ --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active);
}
.fade, .fade-x, .fade-end {
--tw-fade-ltr-end-layer: var(--tw-fade-gradient-r);
--tw-fade-rtl-end-layer: var(--tw-fade-gradient-l);
- --tw-fade-ltr-end-animation: tw-fade-ramp-r;
- --tw-fade-rtl-end-animation: tw-fade-ramp-l-trailing;
+ --tw-fade-ltr-end-animation: tw-fade-travel-r;
+ --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing;
--tw-fade-ltr-end-timeline: scroll(self inline);
--tw-fade-rtl-end-timeline: scroll(self inline);
- --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
- --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
+ --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
}
@supports (animation-timeline: scroll()) {
.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end {
@@ -868,7 +873,7 @@
--tw-fade-size-bottom: initial;
--tw-fade-size-start: initial;
--tw-fade-size-end: initial;
- --tw-fade-ramp: initial;
+ --tw-fade-travel: initial;
--tw-fade-clear: initial;
--tw-fade-clear-y: initial;
--tw-fade-clear-x: initial;
diff --git a/scripts/build-css.mjs b/scripts/build-css.mjs
index 29e8d64..784c0a1 100644
--- a/scripts/build-css.mjs
+++ b/scripts/build-css.mjs
@@ -37,12 +37,12 @@ const src = path.join(root, 'src', 'tw-fade.css')
const outFile = path.join(root, 'dist', 'tw-fade.css')
// Every class the framework-free build should contain. Arbitrary values
-// (fade-size-[6rem], fade-ramp-[80px], fade-clear-top-[56px]) are intentionally
+// (fade-size-[6rem], fade-travel-[80px], fade-clear-top-[56px]) are intentionally
// absent — those require Tailwind's JIT and belong to the v4 source path, not
// the prebuilt drop-in.
const SCALE = 'xs,sm,md,lg,xl,2xl,3xl,4xl'
const CLASSES =
- `fade fade-{x,y,top,bottom,start,end} fade-none fade-none-{x,y} fade-always fade-always-{x,y} fade-size-{${SCALE}} fade-size-{x,y,top,bottom,start,end}-{${SCALE}} fade-ramp-{${SCALE}} fade-clear-{${SCALE},var} fade-clear-{x,y,top,bottom,start,end}-{${SCALE},var}`
+ `fade fade-{x,y,top,bottom,start,end} fade-none fade-none-{x,y} fade-always fade-always-{x,y} fade-size-{${SCALE}} fade-size-{x,y,top,bottom,start,end}-{${SCALE}} fade-travel-{${SCALE}} fade-clear-{${SCALE},var} fade-clear-{x,y,top,bottom,start,end}-{${SCALE},var}`
const BANNER =
'/*! tw-fade — framework-free build for plain HTML / CDN.\n' +
diff --git a/src/tw-fade.css b/src/tw-fade.css
index 1447f94..6c77def 100644
--- a/src/tw-fade.css
+++ b/src/tw-fade.css
@@ -25,7 +25,7 @@
/*
Config: keep "*" with NO initial-value. A typed rejects the rem
fallback and drops the registration in browsers; inherits:false stops nested
-fades from leaking size, ramp, clearance, mask, or routing state.
+fades from leaking size, travel, clearance, mask, or routing state.
*/
@property --tw-fade-size {
syntax: "*";
@@ -55,7 +55,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
syntax: "*";
inherits: false;
}
-@property --tw-fade-ramp {
+@property --tw-fade-travel {
syntax: "*";
inherits: false;
}
@@ -177,14 +177,14 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--fade-size-2xl: calc(var(--spacing, 0.25rem) * 24);
--fade-size-3xl: calc(var(--spacing, 0.25rem) * 32);
--fade-size-4xl: calc(var(--spacing, 0.25rem) * 40);
- --fade-ramp-xs: calc(var(--spacing, 0.25rem) * 6);
- --fade-ramp-sm: calc(var(--spacing, 0.25rem) * 8);
- --fade-ramp-md: calc(var(--spacing, 0.25rem) * 12);
- --fade-ramp-lg: calc(var(--spacing, 0.25rem) * 16);
- --fade-ramp-xl: calc(var(--spacing, 0.25rem) * 20);
- --fade-ramp-2xl: calc(var(--spacing, 0.25rem) * 24);
- --fade-ramp-3xl: calc(var(--spacing, 0.25rem) * 32);
- --fade-ramp-4xl: calc(var(--spacing, 0.25rem) * 40);
+ --fade-travel-xs: calc(var(--spacing, 0.25rem) * 6);
+ --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8);
+ --fade-travel-md: calc(var(--spacing, 0.25rem) * 12);
+ --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16);
+ --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20);
+ --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24);
+ --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32);
+ --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40);
--fade-clear-xs: calc(var(--spacing, 0.25rem) * 6);
--fade-clear-sm: calc(var(--spacing, 0.25rem) * 8);
--fade-clear-md: calc(var(--spacing, 0.25rem) * 12);
@@ -195,7 +195,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--fade-clear-4xl: calc(var(--spacing, 0.25rem) * 40);
}
-@keyframes tw-fade-ramp-t {
+@keyframes tw-fade-travel-t {
from {
--tw-fade-t: 0;
}
@@ -203,7 +203,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-t: 1;
}
}
-@keyframes tw-fade-ramp-b {
+@keyframes tw-fade-travel-b {
from {
--tw-fade-b: 1;
}
@@ -211,7 +211,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-b: 0;
}
}
-@keyframes tw-fade-ramp-l {
+@keyframes tw-fade-travel-l {
from {
--tw-fade-l: 0;
}
@@ -219,7 +219,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-l: 1;
}
}
-@keyframes tw-fade-ramp-r {
+@keyframes tw-fade-travel-r {
from {
--tw-fade-r: 1;
}
@@ -227,7 +227,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-r: 0;
}
}
-@keyframes tw-fade-ramp-r-leading {
+@keyframes tw-fade-travel-r-leading {
from {
--tw-fade-r: 0;
}
@@ -235,7 +235,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-r: 1;
}
}
-@keyframes tw-fade-ramp-l-trailing {
+@keyframes tw-fade-travel-l-trailing {
from {
--tw-fade-l: 1;
}
@@ -265,8 +265,8 @@ fades from leaking size, ramp, clearance, mask, or routing state.
@utility fade-size-end-* {
--tw-fade-size-end: --value(--fade-size-*, [length], [percentage]);
}
-@utility fade-ramp-* {
- --tw-fade-ramp: --value(--fade-ramp-*, [length], [percentage]);
+@utility fade-travel-* {
+ --tw-fade-travel: --value(--fade-travel-*, [length], [percentage]);
}
@utility fade-clear-* {
--tw-fade-clear: calc(var(--spacing, 0.25rem) * --value(integer));
@@ -327,7 +327,7 @@ fades from leaking size, ramp, clearance, mask, or routing state.
.fade-start,
.fade-end {
--tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)));
- --tw-fade-ramp-active: var(--tw-fade-ramp, var(--fade-ramp-md, calc(var(--spacing, 0.25rem) * 12)));
+ --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8)));
--tw-fade-identity-mask: linear-gradient(#000, #000);
--tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
--tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))));
@@ -341,115 +341,131 @@ fades from leaking size, ramp, clearance, mask, or routing state.
--tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b));
--tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l));
--tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r));
+ /*
+ Decouple edge transparency from band growth. --tw-fade-t..r drive the
+ band WIDTH (above), so the band eases open over the full travel distance.
+ But the ALPHA at the very edge is driven by --tw-fade-alpha-*, a
+ saturated copy that reaches full transparency within travel/onset of
+ scroll. This covers the overflow clip almost immediately (no hard cut
+ during early scroll) while the band still widens cosmetically over the
+ whole travel. --tw-fade-onset = how fast the edge goes transparent
+ relative to the travel; higher = snappier clip coverage. At t=0 alpha is
+ also 0, so a flush (unscrolled) edge still shows no fade.
+ */
+ --tw-fade-onset: 8;
+ --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset)));
+ --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset)));
+ --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset)));
+ --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset)));
--tw-fade-gradient-t: linear-gradient(
to bottom,
#000 0 var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017))
calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t))
);
--tw-fade-gradient-b: linear-gradient(
to top,
#000 0 var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017))
calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b))
);
--tw-fade-gradient-l: linear-gradient(
to right,
#000 0 var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017))
calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l))
);
--tw-fade-gradient-r: linear-gradient(
to left,
#000 0 var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r),
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333),
- rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017))
+ rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017))
calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667),
rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r))
);
@@ -510,18 +526,18 @@ fades from leaking size, ramp, clearance, mask, or routing state.
.fade-y,
.fade-top {
--tw-fade-mask-t: var(--tw-fade-gradient-t);
- --tw-fade-animation-t: tw-fade-ramp-t;
+ --tw-fade-animation-t: tw-fade-travel-t;
--tw-fade-timeline-t: scroll(self y);
- --tw-fade-animation-range-t: 0 var(--tw-fade-ramp-active);
+ --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active);
}
.fade,
.fade-y,
.fade-bottom {
--tw-fade-mask-b: var(--tw-fade-gradient-b);
- --tw-fade-animation-b: tw-fade-ramp-b;
+ --tw-fade-animation-b: tw-fade-travel-b;
--tw-fade-timeline-b: scroll(self y);
- --tw-fade-animation-range-b: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%;
}
.fade,
@@ -529,12 +545,12 @@ fades from leaking size, ramp, clearance, mask, or routing state.
.fade-start {
--tw-fade-ltr-start-layer: var(--tw-fade-gradient-l);
--tw-fade-rtl-start-layer: var(--tw-fade-gradient-r);
- --tw-fade-ltr-start-animation: tw-fade-ramp-l;
- --tw-fade-rtl-start-animation: tw-fade-ramp-r-leading;
+ --tw-fade-ltr-start-animation: tw-fade-travel-l;
+ --tw-fade-rtl-start-animation: tw-fade-travel-r-leading;
--tw-fade-ltr-start-timeline: scroll(self inline);
--tw-fade-rtl-start-timeline: scroll(self inline);
- --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-ramp-active);
- --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-ramp-active);
+ --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active);
+ --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active);
}
.fade,
@@ -542,12 +558,12 @@ fades from leaking size, ramp, clearance, mask, or routing state.
.fade-end {
--tw-fade-ltr-end-layer: var(--tw-fade-gradient-r);
--tw-fade-rtl-end-layer: var(--tw-fade-gradient-l);
- --tw-fade-ltr-end-animation: tw-fade-ramp-r;
- --tw-fade-rtl-end-animation: tw-fade-ramp-l-trailing;
+ --tw-fade-ltr-end-animation: tw-fade-travel-r;
+ --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing;
--tw-fade-ltr-end-timeline: scroll(self inline);
--tw-fade-rtl-end-timeline: scroll(self inline);
- --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
- --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-ramp-active)) 100%;
+ --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
+ --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%;
}
@supports (animation-timeline: scroll()) {
diff --git a/test/plugin.test.mjs b/test/plugin.test.mjs
index 1f743b6..e98459c 100644
--- a/test/plugin.test.mjs
+++ b/test/plugin.test.mjs
@@ -18,7 +18,7 @@ const root = path.resolve(__dirname, '..')
const css = compileCss()
const customCss = compileCss({
classes:
- 'fade-size-[15%] fade-size-top-[56px] fade-ramp-[80px] fade-clear-14 fade-clear-y-14 fade-clear-x-14 fade-clear-top-14 fade-clear-bottom-14 fade-clear-start-14 fade-clear-end-14',
+ 'fade-size-[15%] fade-size-top-[56px] fade-travel-[80px] fade-clear-14 fade-clear-y-14 fade-clear-x-14 fade-clear-top-14 fade-clear-bottom-14 fade-clear-start-14 fade-clear-end-14',
})
function block(selector, source = css) {
@@ -89,6 +89,7 @@ const REJECTED_CLASSES = [
'fade-inline-start',
'fade-inline-end',
'fade-range-sm',
+ 'fade-ramp-sm',
'fade-static',
]
const SHARED_SELECTOR = '.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end'
@@ -171,7 +172,7 @@ test('registers amounts as typed numbers and config/routing vars as universal no
assert.equal(prop['initial-value'], '0')
}
- for (const v of [...SIZE_VARS, '--tw-fade-ramp', ...CLEAR_VARS, ...MASK_VARS, ...ROUTING_VARS]) {
+ for (const v of [...SIZE_VARS, '--tw-fade-travel', ...CLEAR_VARS, ...MASK_VARS, ...ROUTING_VARS]) {
const prop = property(v)
assert.ok(prop, `missing @property ${v}`)
assert.equal(prop.syntax, '"*"')
@@ -215,21 +216,25 @@ test('prebuilt dist is generated from the current default compile output', () =>
// Guards against a dormant retired utility name surviving in the shipped CSS.
// The REJECTED_CLASSES checks only prove the *default* compile omits a name; a
// stray @utility / @property / @keyframes / theme token for a retired name
-// ("range" -> "ramp", and the never-shipped intermediate "reveal") could still
-// ship undetected. Scan the source and prebuilt artifacts on disk. Scoped to
-// the CSS files only — README/MIGRATING/CHANGELOG legitimately reference the
-// old names when documenting the rename. Token-specific patterns, so the
-// English verb "reveal" in prose/comments is never matched.
-test('shipped CSS carries no retired reveal/range token names', () => {
+// ("range" -> "ramp" -> "travel", and the never-shipped intermediate "reveal")
+// could still ship undetected. Scan the source and prebuilt artifacts on disk.
+// Scoped to the CSS files only — README/MIGRATING/CHANGELOG legitimately
+// reference the old names when documenting the rename. Token-specific patterns,
+// so the English nouns "reveal"/"ramp" in prose/comments are never matched.
+test('shipped CSS carries no retired reveal/range/ramp token names', () => {
const retired = [
/fade-reveal/,
/fade-range/,
+ /fade-ramp/,
/--fade-reveal/,
/--fade-range/,
+ /--fade-ramp/,
/--tw-fade-reveal/,
/--tw-fade-range/,
+ /--tw-fade-ramp/,
/tw-fade-reveal/,
/tw-fade-range/,
+ /tw-fade-ramp/,
]
for (const file of ['src/tw-fade.css', 'dist/tw-fade.css']) {
const source = fs.readFileSync(path.join(root, file), 'utf8')
@@ -241,16 +246,16 @@ test('shipped CSS carries no retired reveal/range token names', () => {
test('leading and trailing keyframes include RTL horizontal variants', () => {
const kf = (n) => block('@keyframes ' + n)
- assert.match(kf('tw-fade-ramp-t'), /from\s*\{[^}]*--tw-fade-t:\s*0/)
- assert.match(kf('tw-fade-ramp-t'), /to\s*\{[^}]*--tw-fade-t:\s*1/)
- assert.match(kf('tw-fade-ramp-b'), /from\s*\{[^}]*--tw-fade-b:\s*1/)
- assert.match(kf('tw-fade-ramp-b'), /to\s*\{[^}]*--tw-fade-b:\s*0/)
- assert.match(kf('tw-fade-ramp-l'), /from\s*\{[^}]*--tw-fade-l:\s*0/)
- assert.match(kf('tw-fade-ramp-r'), /from\s*\{[^}]*--tw-fade-r:\s*1/)
- assert.match(kf('tw-fade-ramp-r-leading'), /from\s*\{[^}]*--tw-fade-r:\s*0/)
- assert.match(kf('tw-fade-ramp-r-leading'), /to\s*\{[^}]*--tw-fade-r:\s*1/)
- assert.match(kf('tw-fade-ramp-l-trailing'), /from\s*\{[^}]*--tw-fade-l:\s*1/)
- assert.match(kf('tw-fade-ramp-l-trailing'), /to\s*\{[^}]*--tw-fade-l:\s*0/)
+ assert.match(kf('tw-fade-travel-t'), /from\s*\{[^}]*--tw-fade-t:\s*0/)
+ assert.match(kf('tw-fade-travel-t'), /to\s*\{[^}]*--tw-fade-t:\s*1/)
+ assert.match(kf('tw-fade-travel-b'), /from\s*\{[^}]*--tw-fade-b:\s*1/)
+ assert.match(kf('tw-fade-travel-b'), /to\s*\{[^}]*--tw-fade-b:\s*0/)
+ assert.match(kf('tw-fade-travel-l'), /from\s*\{[^}]*--tw-fade-l:\s*0/)
+ assert.match(kf('tw-fade-travel-r'), /from\s*\{[^}]*--tw-fade-r:\s*1/)
+ assert.match(kf('tw-fade-travel-r-leading'), /from\s*\{[^}]*--tw-fade-r:\s*0/)
+ assert.match(kf('tw-fade-travel-r-leading'), /to\s*\{[^}]*--tw-fade-r:\s*1/)
+ assert.match(kf('tw-fade-travel-l-trailing'), /from\s*\{[^}]*--tw-fade-l:\s*1/)
+ assert.match(kf('tw-fade-travel-l-trailing'), /to\s*\{[^}]*--tw-fade-l:\s*0/)
})
test('the shared mask setup owns four physical layers and scroll animation wiring', () => {
@@ -273,27 +278,27 @@ test('the shared mask setup owns four physical layers and scroll animation wirin
test('direction classes select the expected layers and central RTL routing', () => {
const top = block(TOP_SELECTOR)
assert.equal(declValue(top, '--tw-fade-mask-t'), 'var(--tw-fade-gradient-t)')
- assert.equal(declValue(top, '--tw-fade-animation-t'), 'tw-fade-ramp-t')
+ assert.equal(declValue(top, '--tw-fade-animation-t'), 'tw-fade-travel-t')
assert.equal(declValue(top, '--tw-fade-timeline-t'), 'scroll(self y)')
const bottom = block(BOTTOM_SELECTOR)
assert.equal(declValue(bottom, '--tw-fade-mask-b'), 'var(--tw-fade-gradient-b)')
- assert.equal(declValue(bottom, '--tw-fade-animation-b'), 'tw-fade-ramp-b')
+ assert.equal(declValue(bottom, '--tw-fade-animation-b'), 'tw-fade-travel-b')
assert.equal(declValue(bottom, '--tw-fade-timeline-b'), 'scroll(self y)')
const start = block(START_SELECTOR)
assert.equal(declValue(start, '--tw-fade-ltr-start-layer'), 'var(--tw-fade-gradient-l)')
assert.equal(declValue(start, '--tw-fade-rtl-start-layer'), 'var(--tw-fade-gradient-r)')
- assert.equal(declValue(start, '--tw-fade-ltr-start-animation'), 'tw-fade-ramp-l')
- assert.equal(declValue(start, '--tw-fade-rtl-start-animation'), 'tw-fade-ramp-r-leading')
+ assert.equal(declValue(start, '--tw-fade-ltr-start-animation'), 'tw-fade-travel-l')
+ assert.equal(declValue(start, '--tw-fade-rtl-start-animation'), 'tw-fade-travel-r-leading')
assert.equal(declValue(start, '--tw-fade-ltr-start-timeline'), 'scroll(self inline)')
assert.equal(declValue(start, '--tw-fade-rtl-start-timeline'), 'scroll(self inline)')
const end = block(END_SELECTOR)
assert.equal(declValue(end, '--tw-fade-ltr-end-layer'), 'var(--tw-fade-gradient-r)')
assert.equal(declValue(end, '--tw-fade-rtl-end-layer'), 'var(--tw-fade-gradient-l)')
- assert.equal(declValue(end, '--tw-fade-ltr-end-animation'), 'tw-fade-ramp-r')
- assert.equal(declValue(end, '--tw-fade-rtl-end-animation'), 'tw-fade-ramp-l-trailing')
+ assert.equal(declValue(end, '--tw-fade-ltr-end-animation'), 'tw-fade-travel-r')
+ assert.equal(declValue(end, '--tw-fade-rtl-end-animation'), 'tw-fade-travel-l-trailing')
const rtl = block(RTL_SELECTOR)
assert.ok(rtl, 'missing central RTL routing block')
@@ -314,7 +319,7 @@ test('the smoothstep ramp is the 13-stop sigmoid from the reference', () => {
const stops = (grad.match(/rgb\(0 0 0 \//g) || []).length
assert.equal(stops, 13, `${prop} should have 13 colour stops`)
const alphaMultipliers = [
- ...grad.matchAll(new RegExp(`var\\(--tw-fade-${edge}\\)\\s*\\*\\s*([0-9.]+)`, 'g')),
+ ...grad.matchAll(new RegExp(`var\\(--tw-fade-alpha-${edge}\\)\\s*\\*\\s*([0-9.]+)`, 'g')),
].map((m) => m[1])
assert.deepEqual(alphaMultipliers, ALPHA_MULTIPLIERS, `${prop} alpha ramp coefficients must stay exact`)
const positionMultipliers = [
@@ -328,6 +333,45 @@ test('the smoothstep ramp is the 13-stop sigmoid from the reference', () => {
assert.match(declValue(shared, '--tw-fade-gradient-r'), /^linear-gradient\(\s*to left/)
})
+test('edge alpha is decoupled from band growth so the clip is covered before the band finishes widening', () => {
+ const shared = block(SHARED_SELECTOR)
+ assert.ok(shared, 'missing shared block')
+
+ // The onset constant sets how fast the edge reaches full transparency relative
+ // to the travel: full coverage by travel/onset of scroll, independent of band width.
+ assert.equal(declValue(shared, '--tw-fade-onset'), '8')
+
+ for (const edge of ['t', 'b', 'l', 'r']) {
+ // alpha-* is a saturated copy of raw scroll progress (clamped at 1). This is
+ // what drives edge transparency, so the clip is covered almost immediately.
+ assert.equal(
+ declValue(shared, `--tw-fade-alpha-${edge}`),
+ `min(1, calc(var(--tw-fade-${edge}) * var(--tw-fade-onset)))`,
+ `--tw-fade-alpha-${edge} must saturate raw progress`,
+ )
+ // Band WIDTH stays keyed to raw progress, so it still eases open over the
+ // full travel distance (cosmetic widening) while the edge is already covered.
+ assert.equal(
+ declValue(shared, `--tw-fade-edge-band-${edge}`),
+ `calc(var(--tw-fade-edge-size-${edge}) * var(--tw-fade-${edge}))`,
+ `band size for ${edge} must stay on raw --tw-fade-${edge}`,
+ )
+ const grad = declValue(shared, `--tw-fade-gradient-${edge}`)
+ // Every alpha stop reads the saturated alpha var...
+ assert.match(
+ grad,
+ new RegExp(`calc\\(1 - var\\(--tw-fade-alpha-${edge}\\)`),
+ `${edge} gradient alpha must read the saturated alpha var`,
+ )
+ // ...and never raw progress, or the clip would re-couple to the full travel.
+ assert.doesNotMatch(
+ grad,
+ new RegExp(`calc\\(1 - var\\(--tw-fade-${edge}\\)`),
+ `${edge} gradient alpha must not key off raw --tw-fade-${edge}`,
+ )
+ }
+})
+
test('static fallback pins selected fades on when scroll-driven animation is unsupported', () => {
const fallback = block('@supports not (animation-timeline: scroll())')
const shared = block(SHARED_SELECTOR, fallback)
@@ -337,7 +381,7 @@ test('static fallback pins selected fades on when scroll-driven animation is uns
}
})
-test('exposes the size and ramp scales as theme-backed utilities', () => {
+test('exposes the size and travel scales as theme-backed utilities', () => {
for (const [name] of SCALE) {
assert.equal(declValue(block(`.fade-size-${name}`), '--tw-fade-size'), `var(--fade-size-${name})`)
assert.equal(declValue(block(`.fade-size-y-${name}`), '--tw-fade-size-y'), `var(--fade-size-${name})`)
@@ -346,13 +390,13 @@ test('exposes the size and ramp scales as theme-backed utilities', () => {
assert.equal(declValue(block(`.fade-size-bottom-${name}`), '--tw-fade-size-bottom'), `var(--fade-size-${name})`)
assert.equal(declValue(block(`.fade-size-start-${name}`), '--tw-fade-size-start'), `var(--fade-size-${name})`)
assert.equal(declValue(block(`.fade-size-end-${name}`), '--tw-fade-size-end'), `var(--fade-size-${name})`)
- assert.equal(declValue(block(`.fade-ramp-${name}`), '--tw-fade-ramp'), `var(--fade-ramp-${name})`)
+ assert.equal(declValue(block(`.fade-travel-${name}`), '--tw-fade-travel'), `var(--fade-travel-${name})`)
}
const rootBlock = block(':root, :host')
for (const [name, units] of SCALE) {
const expected = `calc(var(--spacing, 0.25rem) * ${units})`
assert.equal(declValue(rootBlock, `--fade-size-${name}`), expected)
- assert.equal(declValue(rootBlock, `--fade-ramp-${name}`), expected)
+ assert.equal(declValue(rootBlock, `--fade-travel-${name}`), expected)
}
assert.equal(block('.fade-range-sm'), null)
assert.equal(block('.fade-size-left-sm'), null)
@@ -379,12 +423,12 @@ test('size resolves as edge over axis over global over capped default', () => {
)
})
-test('arbitrary size and ramp utilities compile only through the source path', () => {
+test('arbitrary size and travel utilities compile only through the source path', () => {
assert.equal(declValue(block('.fade-size-\\[15\\%\\]', customCss), '--tw-fade-size'), '15%')
assert.equal(declValue(block('.fade-size-top-\\[56px\\]', customCss), '--tw-fade-size-top'), '56px')
- assert.equal(declValue(block('.fade-ramp-\\[80px\\]', customCss), '--tw-fade-ramp'), '80px')
+ assert.equal(declValue(block('.fade-travel-\\[80px\\]', customCss), '--tw-fade-travel'), '80px')
assert.equal(block('.fade-size-\\[15\\%\\]'), null)
- assert.equal(block('.fade-ramp-\\[80px\\]'), null)
+ assert.equal(block('.fade-travel-\\[80px\\]'), null)
})
test('exposes directional clear utilities and source-path integer clear values', () => {
From 1184a07b5f281a949dced523ee690a3d8ecb05d8 Mon Sep 17 00:00:00 2001
From: Pete Petrash
Date: Sun, 28 Jun 2026 15:44:32 -0700
Subject: [PATCH 15/17] Add debug pressure-test harness (npm run debug)
scripts/build-debug.mjs assembles a self-contained, gitignored
debug/index.html from scripts/debug/ sources, inlining the compiled
plugin CSS (same output dist/ ships) so the bench always reflects the
real engine.
Synchronized-scroll pressure test: every sample scroller is pinned to
the same offset, so any regression is a side-by-side visual diff. Covers
7 directions + ~16 real-world/edge cases + travel/onset/size/clear
sweeps, driving the engine via public custom props (no class explosion).
onset=1 reproduces the old coupled hard-clip (the A/B); the static-fallback
toggle mirrors the @supports-not path.
Claude-Session: https://claude.ai/code/session_01B1Gn7DoGHbzGCLMbA72Stj
---
.gitignore | 4 +
package.json | 1 +
scripts/build-debug.mjs | 111 ++++++++++
scripts/debug/README.md | 77 +++++++
scripts/debug/harness.css | 354 +++++++++++++++++++++++++++++
scripts/debug/harness.js | 452 ++++++++++++++++++++++++++++++++++++++
6 files changed, 999 insertions(+)
create mode 100644 scripts/build-debug.mjs
create mode 100644 scripts/debug/README.md
create mode 100644 scripts/debug/harness.css
create mode 100644 scripts/debug/harness.js
diff --git a/.gitignore b/.gitignore
index a956bc4..ba65814 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,10 @@ node_modules/
npm-debug.log*
.DS_Store
+# Generated debug harness (maintainer-only dev tool, not shipped).
+# Sources live in scripts/debug/; regenerate with `npm run debug`.
+/debug/
+
# Note: dist/ and demo/styles.css ARE tracked on purpose — they're the
# shipped (npm) and served (demo) artifacts. They're regenerated by
# `npm run build` and `npm run build:demo`, and prepublishOnly rebuilds
diff --git a/package.json b/package.json
index 6260260..65c5c67 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"scripts": {
"build": "node scripts/build-css.mjs",
"build:demo": "tailwindcss -i demo/input.css -o demo/styles.css",
+ "debug": "node scripts/build-debug.mjs",
"export:demo": "node scripts/export-demo-static.mjs",
"ship-demo": "node scripts/ship-demo.mjs",
"generate:seo": "node scripts/generate-seo-assets.mjs",
diff --git a/scripts/build-debug.mjs b/scripts/build-debug.mjs
new file mode 100644
index 0000000..1860a17
--- /dev/null
+++ b/scripts/build-debug.mjs
@@ -0,0 +1,111 @@
+/**
+ * build-debug.mjs — assemble debug/index.html, a self-contained pressure-test
+ * harness for the fade engine.
+ *
+ * node scripts/build-debug.mjs
+ *
+ * It inlines three sources into one standalone file (open via file:// or any
+ * static server, no build step at view time):
+ * 1. the compiled plugin CSS (compileCss() — the SAME output dist/ ships, so
+ * the harness always reflects the real engine, every named utility present);
+ * 2. scripts/debug/harness.css — UI chrome;
+ * 3. scripts/debug/harness.js — scenario config + controls + scroll sync.
+ *
+ * The harness drives the engine through its public custom properties, so this
+ * generator never needs to know the gradient internals — it just ships the
+ * current plugin CSS verbatim.
+ */
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { compileCss } from './build-css.mjs'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const root = path.resolve(__dirname, '..')
+const debugDir = path.join(root, 'debug')
+const outFile = path.join(debugDir, 'index.html')
+
+const pluginCss = compileCss()
+const harnessCss = fs.readFileSync(path.join(__dirname, 'debug', 'harness.css'), 'utf8')
+const harnessJs = fs.readFileSync(path.join(__dirname, 'debug', 'harness.js'), 'utf8')
+
+const ctrl = (label, valId, inner) =>
+ `${inner}`
+
+const panel = `
+
+
+ tw-fade · pressure test
+ every sample is pinned to the same scroll offset — scrub to compare frame-for-frame
+
+
+
+
+ ${ctrl('Fade size', 'sizeVal', '')}
+ ${ctrl('Travel (band open)', 'travelVal', '')}
+ ${ctrl('Onset (edge speed)', 'onsetVal', '')}
+ ${ctrl('Clear', 'clearVal', '')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${ctrl('Content', null, '')}
+ ${ctrl('Background', null, '')}
+ ${ctrl('Container shape', null, '')}
+
+
+
+
+
+
+ onset 1 reproduces the old coupled hard-clip (alpha = t); 8 is the decoupled default.
+ static fallback forces fade-always (the no-scroll-driven-animation path). Set scroll to 8px from start to inspect the onset edge-coverage window; switch to px from end to probe the trailing edge (fade-bottom / fade-end — a separate range).
+
+`
+
+const html = `
+
+
+
+
+tw-fade · pressure test
+
+
+
+${panel}
+
+
+
+
+`
+
+fs.mkdirSync(debugDir, { recursive: true })
+fs.writeFileSync(outFile, html)
+console.log(`wrote ${path.relative(root, outFile)} (${(html.length / 1024).toFixed(1)} KB)`)
diff --git a/scripts/debug/README.md b/scripts/debug/README.md
new file mode 100644
index 0000000..f03231c
--- /dev/null
+++ b/scripts/debug/README.md
@@ -0,0 +1,77 @@
+# tw-fade debug harness
+
+An interactive pressure-test bench for the fade engine. It renders the plugin
+across every direction utility, a battery of real-world / edge-case layouts, and
+four parameter sweeps — all pinned to the **same scroll offset** so any bug is a
+side-by-side visual diff.
+
+## Run
+
+```bash
+npm run debug # → debug/index.html (self-contained, gitignored)
+```
+
+Open `debug/index.html` in a **Chromium-based browser** (needs scroll-driven
+animations + `corner-shape`; Chrome/Edge ≥ 115-ish, full squircle support ≥ 149).
+The file inlines the *compiled* plugin CSS — the same output `dist/` ships — so the
+harness always reflects the real engine. Re-run `npm run debug` after editing
+`src/tw-fade.css`.
+
+## What it exercises
+
+- **Directions** — all seven utilities (`fade`, `fade-y`, `fade-x`, `fade-top`,
+ `fade-bottom`, `fade-start`, `fade-end`) on identical content.
+- **Real-world & edge cases** — short (non-overflowing) content, sticky-header
+ occlusion, padding lead-in, scroll-snap, nested fade scrollers, image/code/dense
+ content, tiny containers, thick borders, oversized (50%) fades, per-edge sizes,
+ `overflow: clip` trap, and the `fade-none` / `fade-none-y` / `fade-always-x`
+ override paths.
+- **Sweeps** — travel, onset, size, clear each across their scale at the global
+ scroll offset.
+
+## Controls
+
+- **Synchronized scroll** — one slider drives every scroller. Fine chips
+ (`0/2/4/6/8/12/24px`) plus a **px-from-start / px-from-end** toggle let you probe
+ the onset edge-coverage window on the leading edge *and* the separate trailing
+ range. `auto` oscillates.
+- **size / travel / onset / clear** sliders write the public custom properties
+ (`--tw-fade-size`, `--tw-fade-travel`, `--tw-fade-onset`, `--tw-fade-clear`)
+ directly on each scroller — no class explosion, continuous values.
+ - **onset 1 reproduces the old coupled hard-clip** (`alpha = t`); 8 is the
+ decoupled default. That is the A/B.
+- **static fallback** forces `fade-always`, mirroring the `@supports not
+ (animation-timeline: scroll())` path that pins amounts to 1.
+- **RTL**, **content type**, **background** (light/dark/checker/photo — reveals the
+ mask silhouette), and **container shape** (rounded/squircle/sharp).
+- Each card shows a live readout: scroll offset, `%`, and the resolved
+ `--tw-fade-{t,b,l,r}` amounts. A non-overflowing card reads `no overflow → no
+ fade` (the key scroll-driven invariant).
+
+## Architecture
+
+Three sources, assembled into one standalone file by `scripts/build-debug.mjs`:
+
+| File | Role |
+| --- | --- |
+| `scripts/build-debug.mjs` | generator — inlines compiled plugin CSS + the two files below, writes `debug/index.html`, emits the control-panel markup |
+| `scripts/debug/harness.css` | UI chrome only (never touches the fade engine) |
+| `scripts/debug/harness.js` | scenario config, controls, scroll-sync engine |
+
+The config props are `@property … inherits:false`, so the harness sets them on
+**each `.fade*` element directly** (a parent would not cascade).
+
+### Add a scenario
+
+Append an object to `SCENARIOS.directions` / `SCENARIOS.realworld` in
+`harness.js`:
+
+```js
+{ id: 'myCase', label: 'My case', cls: 'fade-y', special: 'simple',
+ content: 'text', lock: { travel: 80, vars: { '--tw-fade-size-top': '64px' } },
+ note: 'what to look for' }
+```
+
+`special` selects a builder (`simple`, `plane`, `hscroll`, `sticky`, `leadin`,
+`snap`, `short`, `clip`, `nested`, `bordered`); `lock` overrides global params for
+that card (`size`/`travel`/`onset`/`clear`, or arbitrary inline `vars`).
diff --git a/scripts/debug/harness.css b/scripts/debug/harness.css
new file mode 100644
index 0000000..2f89792
--- /dev/null
+++ b/scripts/debug/harness.css
@@ -0,0 +1,354 @@
+/* tw-fade debug harness — UI chrome only. The plugin CSS is inlined separately
+ * by scripts/build-debug.mjs. Nothing here touches the fade engine; it only
+ * frames the sample scrollers and the control panel. */
+
+:root {
+ --ui-bg: #0d0f14;
+ --ui-panel: #151922;
+ --ui-panel-2: #1c2230;
+ --ui-line: #2a3140;
+ --ui-text: #e6e9ef;
+ --ui-dim: #9aa4b5;
+ --ui-accent: #6ea8fe;
+ --ui-accent-2: #5be0c0;
+ --ui-warn: #ffb454;
+ --stage-bg: #f4f5f7;
+ --stage-card: #ffffff;
+ --stage-text: #1b2430;
+ --stage-dim: #5b6675;
+ --stage-line: #e2e5ea;
+ --scroller-h: 220px;
+}
+
+* { box-sizing: border-box; }
+
+html, body { margin: 0; }
+
+body {
+ background: var(--ui-bg);
+ color: var(--ui-text);
+ font: 13px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+
+a { color: var(--ui-accent); }
+
+/* ---- Control panel ---------------------------------------------------- */
+
+.panel {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ background: color-mix(in srgb, var(--ui-panel) 92%, transparent);
+ backdrop-filter: blur(8px);
+ border-bottom: 1px solid var(--ui-line);
+ padding: 12px 16px;
+}
+
+.panel-title {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ margin: 0 0 10px;
+}
+.panel-title h1 { font-size: 14px; margin: 0; letter-spacing: 0.2px; }
+.panel-title .sub { color: var(--ui-dim); font-size: 12px; }
+.panel-title .spacer { flex: 1; }
+
+.controls {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 14px;
+ align-items: flex-end;
+}
+
+.ctrl { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
+.ctrl > label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.6px;
+ color: var(--ui-dim);
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+}
+.ctrl > label .val { color: var(--ui-accent-2); font-variant-numeric: tabular-nums; }
+
+.ctrl input[type="range"] { width: 168px; accent-color: var(--ui-accent); margin: 4px 0 0; }
+.ctrl select,
+.ctrl input[type="text"] {
+ background: var(--ui-panel-2);
+ color: var(--ui-text);
+ border: 1px solid var(--ui-line);
+ border-radius: 6px;
+ padding: 5px 8px;
+ font: inherit;
+}
+
+.seg { display: inline-flex; gap: 0; border: 1px solid var(--ui-line); border-radius: 7px; overflow: hidden; }
+.seg button {
+ background: var(--ui-panel-2);
+ color: var(--ui-dim);
+ border: 0;
+ border-right: 1px solid var(--ui-line);
+ padding: 5px 9px;
+ font: inherit;
+ cursor: pointer;
+}
+.seg button:last-child { border-right: 0; }
+.seg button:hover { color: var(--ui-text); }
+.seg button[aria-pressed="true"] { background: var(--ui-accent); color: #06121f; font-weight: 600; }
+
+.toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--ui-panel-2);
+ border: 1px solid var(--ui-line);
+ border-radius: 7px;
+ padding: 5px 9px;
+ cursor: pointer;
+ user-select: none;
+}
+.toggle input { accent-color: var(--ui-accent); margin: 0; }
+.toggle.warn[data-on="true"] { border-color: var(--ui-warn); color: var(--ui-warn); }
+
+.btn {
+ background: var(--ui-panel-2);
+ color: var(--ui-text);
+ border: 1px solid var(--ui-line);
+ border-radius: 7px;
+ padding: 6px 11px;
+ font: inherit;
+ cursor: pointer;
+}
+.btn:hover { border-color: var(--ui-accent); }
+.btn.primary { background: var(--ui-accent); color: #06121f; border-color: transparent; font-weight: 600; }
+
+.scrubrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
+.scrubrow .steps { display: inline-flex; gap: 4px; }
+.scrubrow .steps button {
+ background: var(--ui-panel-2);
+ color: var(--ui-dim);
+ border: 1px solid var(--ui-line);
+ border-radius: 6px;
+ padding: 4px 7px;
+ font: 11px/1 ui-monospace, monospace;
+ cursor: pointer;
+}
+.scrubrow .steps button:hover { color: var(--ui-text); border-color: var(--ui-accent); }
+
+.hint { color: var(--ui-dim); font-size: 11px; margin: 8px 0 0; }
+.hint code { color: var(--ui-accent-2); }
+
+/* ---- Stage (sample area) --------------------------------------------- */
+
+.stage { padding: 18px 16px 64px; }
+
+.stage.bg-light { background: var(--stage-bg); }
+.stage.bg-dark { background: #11151c; }
+.stage.bg-check {
+ background:
+ repeating-conic-gradient(#dfe3ea 0 25%, #c5ccd8 0 50%) 0 0 / 22px 22px;
+}
+.stage.bg-photo {
+ background: linear-gradient(125deg, #ff9a9e, #fad0c4 30%, #a1c4fd 65%, #c2e9fb);
+}
+
+.section-h {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ margin: 26px 4px 12px;
+ color: var(--stage-text);
+}
+.stage.bg-dark .section-h, .stage.bg-photo .section-h { color: #fff; }
+.section-h h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 1px; margin: 0; }
+.section-h .desc { font-size: 12px; color: var(--stage-dim); font-weight: 400; text-transform: none; letter-spacing: 0; }
+.stage.bg-dark .section-h .desc, .stage.bg-photo .section-h .desc { color: rgba(255,255,255,0.8); }
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 14px;
+}
+.grid.sweep { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
+
+.card {
+ background: var(--stage-card);
+ border: 1px solid var(--stage-line);
+ border-radius: 12px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
+}
+
+.card-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--stage-line);
+ background: #fafbfc;
+}
+.card-label { font-weight: 600; font-size: 12px; color: var(--stage-text); }
+.card-tag {
+ font: 10px/1 ui-monospace, monospace;
+ background: #eef1f6;
+ color: #46506a;
+ padding: 3px 6px;
+ border-radius: 5px;
+}
+.card-head .readout {
+ margin-left: auto;
+ font: 10px/1 ui-monospace, monospace;
+ color: var(--stage-dim);
+ font-variant-numeric: tabular-nums;
+}
+
+.card-note {
+ padding: 7px 10px;
+ font-size: 11px;
+ color: var(--stage-dim);
+ border-top: 1px solid var(--stage-line);
+ background: #fafbfc;
+}
+
+/* The scroll container that actually carries a fade-* class. Position
+ * relative so the optional band overlay can pin to it. */
+.scroller {
+ position: relative;
+ height: var(--scroller-h);
+ overflow: auto;
+ background: var(--stage-card);
+ color: var(--stage-text);
+ scrollbar-width: thin;
+}
+.scroller.h { overflow: auto; }
+.scroller.short { height: 120px; }
+.scroller.tiny { height: 84px; }
+
+.stage.bg-dark .card, .stage.bg-dark .scroller { background: #1b2230; color: #dfe5ee; border-color: #2a3346; }
+.stage.bg-dark .card-head, .stage.bg-dark .card-note { background: #161d29; border-color: #2a3346; }
+.stage.bg-dark .card-label { color: #eef2f8; }
+
+/* ---- Sample content -------------------------------------------------- */
+
+.body-pad { padding: 14px 16px; }
+.body-pad p { margin: 0 0 11px; }
+.body-pad h3 { margin: 0 0 8px; font-size: 14px; }
+
+.lead-in { padding-top: 48px; padding-bottom: 48px; }
+
+.minilist { padding: 8px; display: flex; flex-direction: column; gap: 8px; }
+.minicard {
+ border: 1px solid var(--stage-line);
+ border-radius: 9px;
+ padding: 10px 12px;
+ background: #fbfcfe;
+}
+.minicard b { display: block; font-size: 12px; }
+.minicard span { color: var(--stage-dim); font-size: 11px; }
+.stage.bg-dark .minicard { background: #222a3a; border-color: #313b50; }
+
+.tilegrid { padding: 10px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
+.tile { aspect-ratio: 4 / 3; border-radius: 9px; }
+
+.code { margin: 0; padding: 12px 14px; font: 11.5px/1.7 ui-monospace, SFMono-Regular, monospace; white-space: pre; color: #2b3850; }
+.stage.bg-dark .code { color: #c7d2e6; }
+
+.dense { padding: 0; }
+.dense .row {
+ display: flex;
+ justify-content: space-between;
+ padding: 7px 12px;
+ border-bottom: 1px solid var(--stage-line);
+ font-size: 12px;
+}
+.dense .row span:last-child { color: var(--stage-dim); font-variant-numeric: tabular-nums; }
+
+/* Horizontal content */
+.scroller.h .track { display: inline-flex; gap: 10px; padding: 14px; white-space: nowrap; height: 100%; align-items: center; }
+.chip {
+ flex: 0 0 auto;
+ border: 1px solid var(--stage-line);
+ border-radius: 999px;
+ padding: 8px 16px;
+ background: #fbfcfe;
+ font-size: 12px;
+ font-weight: 500;
+}
+.stage.bg-dark .chip { background: #222a3a; border-color: #313b50; }
+.hcard {
+ flex: 0 0 180px;
+ height: 150px;
+ border-radius: 11px;
+ padding: 12px;
+ color: #fff;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ font-weight: 600;
+}
+
+/* 2D content for fade (all edges) */
+.plane { width: 760px; padding: 14px; }
+.plane .grid2d { display: grid; grid-template-columns: repeat(8, 84px); gap: 8px; }
+.plane .cell { height: 84px; border-radius: 8px; display: grid; place-items: center; color: #fff; font-weight: 600; }
+
+/* Sticky header inside a scroller */
+.sticky-h {
+ position: sticky;
+ top: 0;
+ background: color-mix(in srgb, var(--stage-card) 86%, transparent);
+ backdrop-filter: blur(4px);
+ padding: 8px 14px;
+ font-weight: 600;
+ font-size: 12px;
+ border-bottom: 1px solid var(--stage-line);
+}
+.stage.bg-dark .sticky-h { background: color-mix(in srgb, #1b2230 86%, transparent); }
+
+/* Nested scroller card */
+.nested-outer { padding: 14px; }
+.nested-inner {
+ height: 150px;
+ overflow: auto;
+ border: 1px dashed var(--stage-line);
+ border-radius: 9px;
+ margin: 10px 0;
+ background: #fbfcfe;
+}
+.stage.bg-dark .nested-inner { background: #222a3a; }
+
+/* Scroll-snap */
+.snap { scroll-snap-type: y mandatory; }
+.snap .snapitem {
+ scroll-snap-align: start;
+ height: 110px;
+ margin: 8px 12px;
+ border-radius: 10px;
+ display: grid;
+ place-items: center;
+ color: #fff;
+ font-weight: 700;
+ font-size: 18px;
+}
+
+/* Container shapes (applied to .scroller via JS shape class) */
+.shape-rounded .scroller { border-radius: 14px; }
+.shape-squircle .scroller { border-radius: 26px; corner-shape: superellipse(1.8); }
+.shape-sharp .scroller { border-radius: 0; }
+.shape-rounded .card, .shape-squircle .card { border-radius: 14px; }
+
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 14px;
+ margin: 6px 4px 0;
+ font-size: 11px;
+ color: var(--stage-dim);
+}
+.stage.bg-dark .legend, .stage.bg-photo .legend { color: rgba(255,255,255,0.85); }
+.legend .sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px; vertical-align: -1px; margin-right: 5px; }
diff --git a/scripts/debug/harness.js b/scripts/debug/harness.js
new file mode 100644
index 0000000..4bd85e0
--- /dev/null
+++ b/scripts/debug/harness.js
@@ -0,0 +1,452 @@
+/* tw-fade debug harness — runtime logic.
+ *
+ * Drives the inlined plugin via the public custom properties only:
+ * --tw-fade-size / --tw-fade-travel / --tw-fade-onset / --tw-fade-clear
+ * (all @property inherits:false, so they are set on each .fade* element
+ * directly, never a parent). Structural scenarios + parameter sweeps are
+ * generated from the SCENARIOS / SWEEPS config below. The single most useful
+ * affordance for a scroll-driven plugin is synchronized scroll: every sample
+ * scroller is pinned to the same offset so you can compare them frame-for-frame.
+ *
+ * Notes baked into the controls:
+ * - onset = 1 reproduces the OLD coupled behavior (alpha = t); 8 is the new
+ * decoupled default. That IS the A/B toggle.
+ * - "Static fallback" forces .fade-always, mirroring the @supports-not path
+ * that pins t/b/l/r = 1 where scroll-driven animation is unsupported.
+ */
+(() => {
+ 'use strict';
+
+ const COLORS = ['#6ea8fe', '#5be0c0', '#ffb454', '#ff7eb6', '#b388ff', '#7ee787', '#f0883e', '#79c0ff'];
+ const grad = (i) => `linear-gradient(135deg, ${COLORS[i % COLORS.length]}, ${COLORS[(i + 3) % COLORS.length]})`;
+
+ const state = {
+ size: null, // null = plugin default; else number (px)
+ travel: null, // null = plugin default; else number (px)
+ onset: 8,
+ clear: 0, // px
+ scrollPct: 0, // 0..1
+ scrollPx: null, // when set, absolute px offset (overrides pct)
+ scrollMode: 'start', // 'start' | 'end' — which side scrollPx is measured from
+ rtl: false,
+ content: 'auto',
+ bg: 'light',
+ shape: 'rounded',
+ fallback: false,
+ };
+
+ let autoTimer = null;
+ let autoDir = 1;
+
+ /* ---- content generators ------------------------------------------- */
+ const LOREM = [
+ 'Scroll-driven masks ride the container’s own scroll position—no JavaScript, no observers.',
+ 'The leading edge is covered the instant you scroll, while the soft band keeps easing open over the travel.',
+ 'Each edge is an independent linear-gradient mask layer, composited with mask-composite: intersect.',
+ 'Because the fade is a mask, whatever sits behind the scroller shows through the faded pixels.',
+ 'Try a checkerboard or photo background to see the exact silhouette the mask carves.',
+ 'Vertical edges stay physical; horizontal start/end edges flip with text direction.',
+ 'A flush, unscrolled edge shows nothing—alpha is zero until the scroll timeline advances.',
+ 'Crank onset down to 1 and the old hard-clip coupling returns: the edge lags the scroll.',
+ ];
+
+ function cText(n) {
+ let h = 'Read me while scrolling
';
+ for (let i = 0; i < n; i++) h += `${LOREM[i % LOREM.length]}
`;
+ return h + '';
+ }
+ function cCards(n) {
+ let h = '';
+ for (let i = 0; i < n; i++) h += `Item ${i + 1}secondary line of supporting metadata`;
+ return h + '';
+ }
+ function cTiles(n) {
+ let h = '';
+ for (let i = 0; i < n; i++) h += ``;
+ return h + '';
+ }
+ function cCode() {
+ const lines = [
+ '',
+ ' ',
+ '',
+ '',
+ '.fade-y {',
+ ' --tw-fade-size: 3rem;',
+ ' --tw-fade-travel: 2rem; /* band open */',
+ ' --tw-fade-onset: 8; /* edge speed */',
+ '}',
+ '',
+ 'fade all four edges',
+ 'fade-y top + bottom',
+ 'fade-x start + end (RTL-aware)',
+ 'fade-top leading vertical only',
+ 'fade-bottom trailing vertical only',
+ ];
+ let body = '';
+ for (let k = 0; k < 3; k++) body += lines.join('\n') + '\n\n';
+ return `${body}`;
+ }
+ function cDense(n) {
+ let h = '';
+ for (let i = 0; i < n; i++) h += `row label ${i + 1}${(Math.sin(i) * 1000 | 0)}`;
+ return h + '';
+ }
+ function cChips(n) {
+ const words = ['All', 'Design', 'Engineering', 'Marketing', 'Sales', 'Support', 'Research', 'Ops', 'Finance', 'Legal', 'People', 'Data', 'Brand', 'Growth'];
+ let h = '';
+ for (let i = 0; i < n; i++) h += `${words[i % words.length]} ${i + 1}`;
+ return h + '';
+ }
+ function cHCards(n) {
+ let h = '';
+ for (let i = 0; i < n; i++) h += `Card ${i + 1}`;
+ return h + '';
+ }
+ function cPlane(cols, rows) {
+ let h = ``;
+ for (let i = 0; i < cols * rows; i++) h += `${i + 1}`;
+ return h + '';
+ }
+
+ const CONTENT = {
+ text: () => cText(10),
+ cards: () => cCards(14),
+ images: () => cTiles(18),
+ code: () => cCode(),
+ dense: () => cDense(28),
+ };
+
+ /* ---- scenarios ----------------------------------------------------- */
+ // kind: 'simple' (scroller + body html) or a named special builder.
+ // flex: body follows the global content-type selector when true.
+ const SCENARIOS = {
+ directions: [
+ { id: 'fy', label: 'fade-y', cls: 'fade-y', body: () => cText(10), flex: true, note: 'Top + bottom. The canonical vertical scroller.' },
+ { id: 'ftop', label: 'fade-top', cls: 'fade-top', body: () => cText(10), flex: true, note: 'Leading edge only; bottom is never masked.' },
+ { id: 'fbot', label: 'fade-bottom', cls: 'fade-bottom', body: () => cText(10), flex: true, note: 'Trailing edge only; only dissolves near the very end.' },
+ { id: 'fall', label: 'fade (all edges)', cls: 'fade', special: 'plane', note: '2-D scroll. Four mask layers intersect at the corners.' },
+ { id: 'fx', label: 'fade-x', cls: 'fade-x', special: 'hscroll', content: 'chips', note: 'Inline start + end. Flips under RTL.' },
+ { id: 'fstart', label: 'fade-start', cls: 'fade-start', special: 'hscroll', content: 'chips', note: 'Inline leading edge. Right edge under RTL.' },
+ { id: 'fend', label: 'fade-end', cls: 'fade-end', special: 'hscroll', content: 'chips', note: 'Inline trailing edge. Left edge under RTL.' },
+ ],
+ realworld: [
+ { id: 'short', label: 'Short content', cls: 'fade-y short', special: 'short', note: 'Does NOT overflow → must show NO fade (scroll-driven path).' },
+ { id: 'sticky', label: 'Sticky header', cls: 'fade-y', special: 'sticky', note: 'Sticky header sits over the faded top edge — watch for double-dim.' },
+ { id: 'leadin', label: 'Padding lead-in', cls: 'fade-y', special: 'leadin', note: 'Top/bottom padding inside the scroller; fade rides the padded edge.' },
+ { id: 'snap', label: 'Scroll-snap', cls: 'fade-y snap', special: 'snap', note: 'scroll-snap-type: y mandatory. Fade should track snapped offsets.' },
+ { id: 'nested', label: 'Nested fade', cls: 'fade-y', special: 'nested', note: 'A fade-y scroller inside a fade-y scroller (block-axis isolation).' },
+ { id: 'images', label: 'Image grid', cls: 'fade-y', special: 'simple', content: 'images', flex: true, note: 'Dense visual content; mask edge crosses tile boundaries.' },
+ { id: 'code', label: 'Code block', cls: 'fade-y', special: 'simple', content: 'code', flex: true, note: 'Monospace, pre-wrapped; check baseline alignment at the edge.' },
+ { id: 'tiny', label: 'Tiny container', cls: 'fade-y tiny', special: 'simple', content: 'dense', note: 'Very short viewport; size may exceed the box.' },
+ { id: 'bordered', label: 'Thick border', cls: 'fade-y', special: 'bordered', note: 'Mask clips the element box — border + fade interaction.' },
+ { id: 'hcards', label: 'Horizontal cards', cls: 'fade-x', special: 'hscroll', content: 'hcards', note: 'Fast flick row; both inline edges fade.' },
+ { id: 'bigsize', label: 'Oversized fade (50%)', cls: 'fade-y', special: 'simple', content: 'text', lock: { vars: { '--tw-fade-size': '50%' } }, note: 'Degenerate: each band is half the viewport. Bands should not cross.' },
+ { id: 'peredge', label: 'Per-edge sizes', cls: 'fade-y', special: 'simple', content: 'text', lock: { vars: { '--tw-fade-size-top': '72px', '--tw-fade-size-bottom': '16px' } }, note: 'top 72px / bottom 16px — asymmetric per-edge sizing.' },
+ { id: 'clip', label: 'overflow: clip (trap)', cls: 'fade-y', special: 'clip', content: 'text', note: 'overflow:clip is not a scroll container → timeline inert → NO fade though content IS clipped.' },
+ { id: 'fnone', label: 'fade-none (control)', cls: 'fade-y fade-none', special: 'simple', content: 'text', noFallback: true, note: 'Forced off — must never fade, even mid-scroll.' },
+ { id: 'noney', label: 'fade-none-y on fade', cls: 'fade fade-none-y', special: 'plane', noFallback: true, note: 'All edges armed, vertical forced off → only the inline (l/r) edges fade.' },
+ { id: 'alwaysx', label: 'fade-always-x on fade-y', cls: 'fade-y fade-always-x', special: 'simple', content: 'text', noFallback: true, note: 'l/r pinned to 1 but fade-y has no horizontal layer → identity mask, NO horizontal fade.' },
+ ],
+ };
+
+ // One parameter swept across its scale, all cells at the global scroll offset.
+ const SWEEPS = [
+ {
+ title: 'Travel sweep', key: 'travel', unit: 'px',
+ desc: 'Band-open speed. With the new decouple, the edge is covered immediately at every travel — only the soft band widens. Scrub to ~8px to compare.',
+ values: [12, 24, 48, 80, 120, 160],
+ },
+ {
+ title: 'Onset sweep', key: 'onset', unit: '',
+ desc: 'Edge-coverage speed. onset 1 = the OLD coupled hard-clip; 8 = current default. Scrub to ~8px to see the difference.',
+ values: [1, 2, 4, 8, 12, 24],
+ },
+ {
+ title: 'Size sweep', key: 'size', unit: 'px',
+ desc: 'Band width at full fade. Pure cosmetics — does not affect when the clip is covered.',
+ values: [24, 32, 48, 64, 96, 128],
+ },
+ {
+ title: 'Clear sweep', key: 'clear', unit: 'px',
+ desc: 'Untouched gap before the band starts. Useful to keep a pinned first row crisp.',
+ values: [0, 8, 16, 24, 40],
+ },
+ ];
+
+ /* ---- DOM building -------------------------------------------------- */
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+
+ function bodyHtmlFor(scn) {
+ const content = (state.content !== 'auto' && scn.flex) ? state.content : scn.content;
+ if (content && CONTENT[content]) return CONTENT[content]();
+ if (content === 'chips') return cChips(26);
+ if (content === 'hcards') return cHCards(12);
+ return cText(10);
+ }
+
+ function buildScroller(scn) {
+ const lock = scn.lock ? JSON.stringify(scn.lock) : '{}';
+ const extra = scn.noFallback ? ' data-nofallback="1"' : '';
+ const sp = scn.special;
+ let cls = 'scroller ' + scn.cls;
+ let styleAttr = '';
+ let inner;
+
+ if (sp === 'plane') { cls += ' h'; inner = cPlane(8, 8); }
+ else if (sp === 'clip') { styleAttr = ' style="overflow:clip"'; inner = bodyHtmlFor(scn); }
+ else if (sp === 'hscroll') { cls += ' h'; inner = bodyHtmlFor(scn); }
+ else if (sp === 'sticky') {
+ inner = 'Sticky header · stays pinned' + cCards(14);
+ }
+ else if (sp === 'leadin') { inner = '' + LOREM.join('
') + '
'; }
+ else if (sp === 'snap') {
+ let s = '';
+ for (let i = 0; i < 10; i++) s += `${i + 1}`;
+ inner = s + '';
+ }
+ else if (sp === 'short') { inner = '' + LOREM[0] + '
'; }
+ else { inner = bodyHtmlFor(scn); }
+
+ let html = `${inner}`;
+
+ if (sp === 'nested') {
+ html = ``
+ + 'Outer scroller. The inner box is its own fade-y scroll context.
'
+ + '' + cDense(18) + ''
+ + 'After the nested region the outer keeps scrolling so its own bottom edge fades.
'
+ + cText(6).replace('', '')
+ + '';
+ }
+ if (sp === 'bordered') {
+ html = `${cCards(14)}`;
+ }
+ return html;
+ }
+
+ function buildCard(scn) {
+ return ``
+ + `${esc(scn.label)}`
+ + `${esc(scn.cls.replace('scroller', '').trim())}`
+ + ''
+ + buildScroller(scn)
+ + `${esc(scn.note || '')}`
+ + '';
+ }
+
+ function buildSweepCard(sweep, value) {
+ const lock = { [sweep.key]: value };
+ const label = `${sweep.key} ${value}${sweep.unit}` + (sweep.key === 'onset' && value === 1 ? ' (old)' : (sweep.key === 'onset' && value === 8 ? ' (default)' : ''));
+ return `${esc(label)}`
+ + `${cText(9)}`
+ + '';
+ }
+
+ function render() {
+ const stage = document.getElementById('stage');
+ let h = '';
+
+ h += 'Directions
all seven public direction utilities on identical content';
+ h += '' + SCENARIOS.directions.map(buildCard).join('') + '';
+
+ h += 'Real-world & edge cases
layouts and content that have historically broken edge-fade plugins';
+ h += '' + SCENARIOS.realworld.map(buildCard).join('') + '';
+
+ SWEEPS.forEach((sw) => {
+ h += `${esc(sw.title)}
${esc(sw.desc)}`;
+ h += '' + sw.values.map((v) => buildSweepCard(sw, v)).join('') + '';
+ });
+
+ stage.innerHTML = h;
+ applyStageChrome();
+ applyAll();
+ syncScroll();
+ }
+
+ /* ---- applying parameters ------------------------------------------ */
+ function setVarPx(el, prop, value) {
+ if (value == null || value === '') el.style.removeProperty(prop);
+ else el.style.setProperty(prop, typeof value === 'number' ? value + 'px' : value);
+ }
+
+ function applyTo(el) {
+ const lock = JSON.parse(el.dataset.lock || '{}');
+ const p = {
+ size: 'size' in lock ? lock.size : state.size,
+ travel: 'travel' in lock ? lock.travel : state.travel,
+ onset: 'onset' in lock ? lock.onset : state.onset,
+ clear: 'clear' in lock ? lock.clear : state.clear,
+ };
+ setVarPx(el, '--tw-fade-size', p.size);
+ setVarPx(el, '--tw-fade-travel', p.travel);
+ el.style.setProperty('--tw-fade-onset', String(p.onset));
+ setVarPx(el, '--tw-fade-clear', p.clear);
+ if (lock.vars) for (const k in lock.vars) el.style.setProperty(k, lock.vars[k]);
+ const allowFallback = state.fallback && !el.dataset.nofallback;
+ el.classList.toggle('fade-always', allowFallback);
+ }
+
+ function applyAll() {
+ document.querySelectorAll('.scroller').forEach(applyTo);
+ }
+
+ function applyStageChrome() {
+ const stage = document.getElementById('stage');
+ stage.className = 'stage bg-' + state.bg + ' shape-' + state.shape;
+ const grid = stage;
+ grid.dir = state.rtl ? 'rtl' : 'ltr';
+ }
+
+ /* ---- scroll sync --------------------------------------------------- */
+ function syncScroll() {
+ document.querySelectorAll('.scroller').forEach((el) => {
+ const maxY = el.scrollHeight - el.clientHeight;
+ const maxX = el.scrollWidth - el.clientWidth;
+ const sign = state.rtl ? -1 : 1;
+ if (state.scrollPx != null) {
+ const fromEnd = state.scrollMode === 'end';
+ const posY = fromEnd ? maxY - state.scrollPx : state.scrollPx;
+ const posX = fromEnd ? maxX - state.scrollPx : state.scrollPx;
+ if (maxY > 0) el.scrollTop = Math.max(0, Math.min(posY, maxY));
+ if (maxX > 0) el.scrollLeft = sign * Math.max(0, Math.min(posX, maxX));
+ } else {
+ if (maxY > 0) el.scrollTop = Math.round(state.scrollPct * maxY);
+ if (maxX > 0) el.scrollLeft = sign * Math.round(state.scrollPct * maxX);
+ }
+ });
+ requestAnimationFrame(() => requestAnimationFrame(updateReadouts));
+ }
+
+ function updateReadouts() {
+ document.querySelectorAll('.card').forEach((card) => {
+ const out = card.querySelector('.readout');
+ const el = card.querySelector('.scroller');
+ if (!out || !el) return;
+ const maxY = el.scrollHeight - el.clientHeight;
+ const maxX = el.scrollWidth - el.clientWidth;
+ const cs = getComputedStyle(el);
+ const amt = (p) => (Number(cs.getPropertyValue('--tw-fade-' + p)) || 0).toFixed(2).replace(/^0(?=\.)/, '');
+ if (maxX > maxY && maxX > 0) {
+ const x = Math.abs(el.scrollLeft);
+ out.textContent = `x ${x|0}/${maxX|0} · ${Math.round(x / maxX * 100)}% · l${amt('l')} r${amt('r')}`;
+ } else if (maxY > 0) {
+ out.textContent = `y ${el.scrollTop|0}/${maxY|0} · ${Math.round(el.scrollTop / maxY * 100)}% · t${amt('t')} b${amt('b')}`;
+ } else {
+ out.textContent = 'no overflow → no fade';
+ }
+ });
+ }
+
+ /* ---- auto-scroll --------------------------------------------------- */
+ function toggleAuto(on) {
+ if (autoTimer) { cancelAnimationFrame(autoTimer); autoTimer = null; }
+ if (!on) return;
+ const tick = () => {
+ state.scrollPx = null;
+ state.scrollPct += autoDir * 0.004;
+ if (state.scrollPct >= 1) { state.scrollPct = 1; autoDir = -1; }
+ if (state.scrollPct <= 0) { state.scrollPct = 0; autoDir = 1; }
+ const slider = document.getElementById('scrollRange');
+ if (slider) slider.value = String(Math.round(state.scrollPct * 100));
+ syncScroll();
+ autoTimer = requestAnimationFrame(tick);
+ };
+ autoTimer = requestAnimationFrame(tick);
+ }
+
+ /* ---- controls ------------------------------------------------------ */
+ function fmtSize(v) { return v == null ? 'default' : v + 'px'; }
+
+ function wire() {
+ const $ = (id) => document.getElementById(id);
+
+ const bind = (id, fn) => { const e = $(id); if (e) e.addEventListener('input', fn); };
+
+ bind('sizeRange', (e) => {
+ state.size = e.target.value === '0' ? null : Number(e.target.value);
+ $('sizeVal').textContent = fmtSize(state.size);
+ applyAll();
+ });
+ bind('travelRange', (e) => {
+ state.travel = e.target.value === '0' ? null : Number(e.target.value);
+ $('travelVal').textContent = fmtSize(state.travel);
+ applyAll(); syncScroll();
+ });
+ bind('onsetRange', (e) => {
+ state.onset = Number(e.target.value);
+ $('onsetVal').textContent = String(state.onset) + (state.onset === 1 ? ' (old)' : '');
+ applyAll();
+ });
+ bind('clearRange', (e) => {
+ state.clear = Number(e.target.value);
+ $('clearVal').textContent = state.clear + 'px';
+ applyAll();
+ });
+
+ bind('scrollRange', (e) => {
+ toggleAuto(false); $('autoChk').checked = false;
+ state.scrollPx = null;
+ state.scrollPct = Number(e.target.value) / 100;
+ syncScroll();
+ });
+
+ document.querySelectorAll('[data-scrollmode]').forEach((b) => b.addEventListener('click', () => {
+ state.scrollMode = b.dataset.scrollmode;
+ document.querySelectorAll('[data-scrollmode]').forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
+ if (state.scrollPx != null) syncScroll();
+ }));
+
+ document.querySelectorAll('[data-step]').forEach((b) => b.addEventListener('click', () => {
+ toggleAuto(false); $('autoChk').checked = false;
+ const v = b.dataset.step;
+ if (v.endsWith('%')) { state.scrollPx = null; state.scrollPct = parseInt(v) / 100; $('scrollRange').value = parseInt(v); }
+ else { state.scrollPx = Number(v); }
+ syncScroll();
+ }));
+
+ $('autoChk').addEventListener('change', (e) => toggleAuto(e.target.checked));
+
+ const reRender = () => render();
+ $('contentSel').addEventListener('change', (e) => { state.content = e.target.value; reRender(); });
+ $('bgSel').addEventListener('change', (e) => { state.bg = e.target.value; applyStageChrome(); });
+ $('shapeSel').addEventListener('change', (e) => { state.shape = e.target.value; applyStageChrome(); });
+
+ $('rtlChk').addEventListener('change', (e) => { state.rtl = e.target.checked; applyStageChrome(); syncScroll(); });
+ $('fallbackChk').addEventListener('change', (e) => {
+ state.fallback = e.target.checked;
+ e.target.closest('.toggle').dataset.on = String(state.fallback);
+ applyAll();
+ });
+ $('resetBtn').addEventListener('click', () => {
+ Object.assign(state, { size: null, travel: null, onset: 8, clear: 0, scrollPct: 0, scrollPx: null, fallback: false });
+ toggleAuto(false);
+ document.querySelectorAll('input,select').forEach((el) => {
+ if (el.type === 'checkbox') el.checked = false;
+ });
+ $('sizeRange').value = 0; $('sizeVal').textContent = 'default';
+ $('travelRange').value = 0; $('travelVal').textContent = 'default';
+ $('onsetRange').value = 8; $('onsetVal').textContent = '8';
+ $('clearRange').value = 0; $('clearVal').textContent = '0px';
+ $('scrollRange').value = 0;
+ render();
+ });
+
+ // Re-sync on real user scroll of any single card (so readouts stay honest)
+ document.getElementById('stage').addEventListener('scroll', (e) => {
+ if (e.target.classList && e.target.classList.contains('scroller')) {
+ requestAnimationFrame(updateReadouts);
+ }
+ }, true);
+
+ window.addEventListener('resize', () => syncScroll());
+ }
+
+ /* ---- init ---------------------------------------------------------- */
+ function init() {
+ render();
+ wire();
+ }
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
+ else init();
+})();
From 27e813af08c8a413c6ba6e189c5c0971136fa134 Mon Sep 17 00:00:00 2001
From: Pete Petrash
Date: Sun, 28 Jun 2026 20:23:47 -0700
Subject: [PATCH 16/17] Fix three doc inaccuracies found in pre-publish review
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- README: the prebuilt-CSS exclusion note listed "integer fade-size-*
classes" — there is no integer fade-size form; fade-clear-* is the one
with an integer scale. Corrected to fade-clear-*.
- README: --tw-fade-onset is documented as a public edge-speed knob under
Travel Distance, but the public-surface note declared the entire
--tw-fade-* namespace internal — a contradiction. Carve onset out as the
documented exception.
- MIGRATING: note that the default travel changed md → sm (cosmetic, no
action needed). It was documented only in the changelog.
Found by the 6-dimension pre-publish review; all minor/nit, no behavior change.
Claude-Session: https://claude.ai/code/session_01B1Gn7DoGHbzGCLMbA72Stj
---
MIGRATING.md | 2 +-
README.md | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/MIGRATING.md b/MIGRATING.md
index 552c326..f3a6e16 100644
--- a/MIGRATING.md
+++ b/MIGRATING.md
@@ -75,7 +75,7 @@ The suffix may be a named scale (`-md`), a bare integer (`-2`), or an arbitrary
| `fade-clear-xy-` | `fade-clear-` |
| `fade-size-x-` · `fade-size-y-` · `fade-clear-x-` · `fade-clear-y-` | _unchanged_ |
-`range` → `travel` controls how quickly the soft band eases open as you scroll (the masked edge itself is now covered almost immediately — see the [changelog](./CHANGELOG.md)). `fade-travel-*` is **global only** — no per-edge travel utilities.
+`range` → `travel` controls how quickly the soft band eases open as you scroll (the masked edge itself is now covered almost immediately — see the [changelog](./CHANGELOG.md)). `fade-travel-*` is **global only** — no per-edge travel utilities. The default travel is now `sm` (was `md`), so a surface that relied on the implicit default gets a slightly snappier band open — purely cosmetic, no action needed.
---
diff --git a/README.md b/README.md
index 6ac7d03..928e2aa 100644
--- a/README.md
+++ b/README.md
@@ -255,7 +255,7 @@ Two details matter:
| Arbitrary values like `fade-size-[6rem]` | yes | no |
| Integer clear values like `fade-clear-top-14` | yes | no |
-The prebuilt file is generated from an explicit safelist. It does not include Tailwind Preflight, core Tailwind utilities, arbitrary values, or integer `fade-size-*` classes.
+The prebuilt file is generated from an explicit safelist. It does not include Tailwind Preflight, core Tailwind utilities, arbitrary values, or integer `fade-clear-*` classes.
## Exports
@@ -284,7 +284,7 @@ If an axis cannot scroll, that axis stays at `0`, so the fade does not show. Thi
In browsers without scroll-driven animations, selected fades pin fully on as a static fallback.
-The supported public surface is the `fade-*` utilities and the public `--fade-*` tokens described above. Treat `--tw-fade-*` as internal implementation detail.
+The supported public surface is the `fade-*` utilities, the public `--fade-*` tokens described above, and `--tw-fade-onset` (the edge-speed knob documented under [Travel Distance](#travel-distance)). Treat the rest of the `--tw-fade-*` namespace as internal implementation detail.
## RTL
From 182406a02a440d55aca38b28a5b523dfba18848e Mon Sep 17 00:00:00 2001
From: Pete Petrash
Date: Sun, 28 Jun 2026 22:36:22 -0700
Subject: [PATCH 17/17] Release 0.7.0
Set the CHANGELOG 0.7.0 date to the actual publish date (the entry shipped
with a 2026-06-26 placeholder). dist/demo/version were already current at the
prior commit, so this carries only the date correction.
Claude-Session: https://claude.ai/code/session_01B1Gn7DoGHbzGCLMbA72Stj
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0f382b..62e205b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [0.7.0] - 2026-06-26
+## [0.7.0] - 2026-06-28
Breaking rename to a plain direction API. [MIGRATING.md](./MIGRATING.md) is an
agent-ready upgrade procedure (ordered renames, RTL caveats, and a verification grep);