I'm facing an issue where animations with translateY()
refuse to trigger when the component has an initial state of translate-y-full
.
Here's some example code:
const { ref: refHeading, inView: inViewHeading } = useInView();
const { locale } = useLocale();
return (
<>
{/* contact */}
<div ref={refHeading} id="contact" className="bg-bg w-full">
<div className="p-page">
<Text type="large" animate={inViewHeading} className="w-[60%]">{strings[locale]["contact_row_1"]}</Text>{" "}
<Text type="large" animate={inViewHeading} className="w-[60%]">{strings[locale]["contact_row_2"]}</Text>{" "}
<Text type="large" animate={inViewHeading} className="w-[60%] inline-block">{strings[locale]["contact_row_inline"]}</Text>
{/* ... more code here */}
</div>
</div>
</>
);
My Text
component just renders a <h1>
with a <div>
around it with overflow-hidden
:
import PropTypes from "prop-types";
function Text({ children, type = "pg", className = "", link = false, animate = true, gradual = true, ...props }) {
// type accepts "large", "title", "pg", "sub"
if (!children) {
throw new Error("no children?");
}
let Component = "";
let cn = className.trim();
let animation = "";
if (animate) {
animation += gradual ? "animate-slide-up-gradual" : "animate-slide-up";
} else {
animation = "";
}
switch (type) {
case "large":
Component = "h1";
cn += " tracking-all text-text text-large leading-none font-semibold";
break;
case "title":
Component = "h2";
cn += " tracking-all text-text text-title leading-none font-bold";
break;
case "pg":
Component = "p";
cn += " tracking-all text-text text-pg leading-tight font-medium";
break;
case "sub":
Component = "span"
cn += " tracking-all text-text text-sub leading-none font-semibold";
animation += " inline-block";
break;
default:
throw new Error("invalid text type");
}
if (link) {
Component = "a";
animation += " inline-block";
console.log(cn)
}
return (
<div className={`overflow-hidden ${cn.trim()}`}>
<Component className={animation} {...props}>{children}</Component>
</div>
);
}
My CSS animations:
.animate-slide-up {
animation: slide-up 0.5s cubic-bezier(0.3, 0, 0.05, 1) 0.2s both;
}
.animate-slide-up-gradual {
animation: slide-up 0.75s cubic-bezier(0.15, 0, 0.05, 1) 0.2s both;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
At this point, the code works completely fine! Animations trigger, however the animations "flicker" for a bit when you first scroll down to the components.
I believe this is due to the components having translateY(0)
by default, then after a while the animations are triggered, when at this point the translateY(100%)
kicks in and it finally disappears.
I tried solving this through two methods:
!important
to the animate classes' transformstranslate-y-full
to the Text's Component component by defaultHowever, both of these approaches resulted in text that was stuck hidden, and the animations wouldn't trigger at all:
My questions are, why does this occur? And are there any more elegant solutions to prevent this animation "flicker"?
The component first shows up in translateY(0)
position. Later, the animate-slide-up
class is added when inView
becomes true
. This creates a flicker, because the element is already visible, then it suddenly jumps down when the animation starts from translateY(100%)
, and finally it moves back up to translateY(0)
.
To avoid the flickering, the most important thing is that the component's initial state should already be translateY(100%)
, and it shouldn't suddenly change when the animation starts.
if (animate) {
animation += gradual ? "animate-slide-up-gradual" : "animate-slide-up";
} else {
animation = "translate-y-full"; /* set translate-y to 100% by default without animation classes */
}
adding
translate-y-full
to the Text's Component component by default
If you add it directly to the Text
component without using an if condition, the styles will unfortunately conflict, and .translate-y-full
might apply a stronger rule that overrides the animation.
The strength of CSS classes is not determined by the order in which they appear inside class="..."
, but by their CSS specificity and the layer they belong to. In TailwindCSS, all utility classes are placed in the utilities layer, which is the strongest layer, but within it, all utilities have equal strength. You can read more about this in this question:
So you can use the !important modifier, which:
For TailwindCSS v4
<Text class="animate-slide-up! translate-y-full">
For TailwindCSS v3
<Text class="!animate-slide-up translate-y-full">
Or you can manually increase the specificity of the given class - to fully understand this solution, it's helpful to know how CSS specificity is calculated for each declaration -:
<Text class="[&]:animate-slide-up translate-y-full">
Important Note: Actually, the proper declaration is the correct (and recommended) solution.
But these only work as expected if you declare the animations using the TailwindCSS utility system; otherwise, they will fail similarly to the other attempt (see below).
@theme {
--animate-slide-up: slide-up 0.5s cubic-bezier(0.3, 0, 0.05, 1) 0.2s both;
--animate-slide-up-gradual: slide-up 0.75s cubic-bezier(0.15, 0, 0.05, 1) 0.2s both;
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
}
With proper declaration, TailwindCSS places animations at the end of the utilities layer, so due to the order of definition, the animation automatically overrides the translate-y-*
classes as well.
Working as expected without any modifier
<Text class="animate-slide-up translate-y-full">
adding
!important
to the animate classes' transforms
Although I'm generally against using !important
, in this case, you need to apply it directly to the transform property for it to work as expected.
.animate-slide-up {
animation: slide-up 0.5s cubic-bezier(0.3, 0, 0.05, 1) 0.2s both;
}
.animate-slide-up-gradual {
animation: slide-up 0.75s cubic-bezier(0.15, 0, 0.05, 1) 0.2s both;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0) !important;
}
}
Although this doesn't solve the initial flickering either.