tailwindcss-motion
Version:
Tailwind Motion is a Tailwind CSS Plugin made by Rombo. It’s a simple, yet powerful, animation library for Tailwind CSS.
328 lines (309 loc) • 10.4 kB
text/typescript
import type { CSSRuleObject, PluginAPI } from "tailwindcss/types/config.js";
type SpringMultipliers = {
[key: string]: string;
};
type ThemeConfig = {
motionDelay: Record<string, string>;
motionDuration: Record<string, string>;
motionLoopCount: Record<string, string>;
motionTimingFunction: Record<string, string>;
transitionDuration: Record<string, string>;
transitionTimingFunction: Record<string, string>;
};
type UtilityOptions = {
modifier: string | null;
};
// Define spring types and their corresponding perceptual duration multipliers
export const springPerceptualMultipliers: SpringMultipliers = {
"var(--motion-spring-smooth)": "1.66",
"var(--motion-spring-snappy)": "1.66",
"var(--motion-spring-bouncy)": "1.66",
"var(--motion-spring-bouncier)": "2.035",
"var(--motion-spring-bounciest)": "5.285",
"var(--motion-bounce)": "2",
};
export function addModifiers(
matchUtilities: PluginAPI["matchUtilities"],
addUtilities: PluginAPI["addUtilities"],
theme: (path: keyof ThemeConfig) => Record<string, string>
) {
// duration
matchUtilities(
{
"motion-duration": (value: string, { modifier }: UtilityOptions) => {
switch (modifier) {
case "scale":
return { "--motion-scale-duration": value } as CSSRuleObject;
case "translate":
return { "--motion-translate-duration": value };
case "rotate":
return { "--motion-rotate-duration": value };
case "blur":
case "grayscale":
return { "--motion-filter-duration": value };
case "opacity":
return { "--motion-opacity-duration": value };
case "background":
return { "--motion-background-color-duration": value };
case "text":
return { "--motion-text-color-duration": value };
default:
return {
"--motion-duration": value,
};
}
},
},
{
values: theme("motionDuration"),
modifiers: {
scale: "scale",
translate: "translate",
rotate: "rotate",
blur: "blur",
grayscale: "grayscale",
opacity: "opacity",
background: "background",
text: "text",
},
}
);
// delay
matchUtilities(
{
"motion-delay": (value: string, { modifier }: UtilityOptions) => {
switch (modifier) {
case "scale":
return { "--motion-scale-delay": value } as CSSRuleObject;
case "translate":
return { "--motion-translate-delay": value };
case "rotate":
return { "--motion-rotate-delay": value };
case "blur":
case "grayscale":
return { "--motion-filter-delay": value };
case "opacity":
return { "--motion-opacity-delay": value };
case "background":
return { "--motion-background-color-delay": value };
case "text":
return { "--motion-text-color-delay": value };
default:
return {
"--motion-delay": value,
};
}
},
},
{
values: theme("motionDelay"),
modifiers: {
scale: "scale",
translate: "translate",
rotate: "rotate",
blur: "blur",
grayscale: "grayscale",
opacity: "opacity",
background: "background",
text: "text",
},
}
);
// ease
matchUtilities(
{
"motion-ease": (value: string, { modifier }: UtilityOptions) => {
// if the ease isn't a spring, the multiplier doesn't change anything
const perceptualDurationMultiplier =
springPerceptualMultipliers[value] || 1;
const isSpringWithBounce = [
"var(--motion-spring-bouncy)",
"var(--motion-spring-bouncier)",
"var(--motion-spring-bounciest)",
"var(--motion-bounce)",
].includes(value);
switch (modifier) {
case "scale":
return {
"--motion-scale-timing": value,
"--motion-scale-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
} as CSSRuleObject;
case "translate":
return {
"--motion-translate-timing": value,
"--motion-translate-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
case "rotate":
return {
"--motion-rotate-timing": value,
"--motion-rotate-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
case "blur":
case "grayscale":
return {
"--motion-filter-timing": value,
"--motion-filter-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
case "opacity":
return {
"--motion-opacity-timing": value,
"--motion-opacity-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
case "background":
return {
"--motion-background-color-timing": value,
"--motion-background-color-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
case "text":
return {
"--motion-text-color-timing": value,
"--motion-text-color-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
default:
if (isSpringWithBounce) {
return {
"--motion-timing": value,
"--motion-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
// filter, opacity, and color animations don't look good with bouncy springs
// so we use a smooth spring for them
"--motion-filter-timing": "var(--motion-spring-smooth)",
"--motion-opacity-timing": "var(--motion-spring-smooth)",
"--motion-background-color-timing":
"var(--motion-spring-smooth)",
"--motion-text-color-timing": "var(--motion-spring-smooth)",
"--motion-filter-perceptual-duration-multiplier": "1.66",
"--motion-opacity-perceptual-duration-multiplier": "1.66",
"--motion-background-color-perceptual-duration-multiplier":
"1.66",
"--motion-text-color-perceptual-duration-multiplier": "1.66",
};
} else {
return {
"--motion-timing": value,
"--motion-perceptual-duration-multiplier": `${perceptualDurationMultiplier}`,
};
}
}
},
},
{
values: theme("motionTimingFunction"),
modifiers: {
scale: "scale",
translate: "translate",
rotate: "rotate",
blur: "blur",
grayscale: "grayscale",
opacity: "opacity",
background: "background",
text: "text",
},
}
);
// animation play state
addUtilities({
".motion-paused": {
animationPlayState: "paused",
"&::before": {
animationPlayState: "paused",
},
"&::after": {
animationPlayState: "paused",
},
},
".motion-running": {
animationPlayState: "running",
"&::before": {
animationPlayState: "running",
},
"&::after": {
animationPlayState: "running",
},
},
});
// loop
matchUtilities(
{
"motion-loop": (value: string, { modifier }: UtilityOptions) => {
switch (modifier) {
case "scale":
return { "--motion-scale-loop-count": value } as CSSRuleObject;
case "translate":
return { "--motion-translate-loop-count": value };
case "rotate":
return { "--motion-rotate-loop-count": value };
case "blur":
case "grayscale":
return { "--motion-filter-loop-count": value };
case "opacity":
return { "--motion-opacity-loop-count": value };
case "background":
return { "--motion-background-color-loop-count": value };
case "text":
return { "--motion-text-color-loop-count": value };
default:
return {
"--motion-loop-count": value,
};
}
},
},
{
values: theme("motionLoopCount"),
modifiers: {
scale: "scale",
translate: "translate",
rotate: "rotate",
blur: "blur",
grayscale: "grayscale",
opacity: "opacity",
background: "background",
text: "text",
},
}
);
}
export const modifiersTheme = {
motionTimingFunction: (
theme: (path: keyof ThemeConfig) => Record<string, string>
) => ({
...theme("transitionTimingFunction"),
"spring-smooth": "var(--motion-spring-smooth)",
"spring-snappy": "var(--motion-spring-snappy)",
"spring-bouncy": "var(--motion-spring-bouncy)",
"spring-bouncier": "var(--motion-spring-bouncier)",
"spring-bounciest": "var(--motion-spring-bounciest)",
bounce: "var(--motion-bounce)",
"in-quad": "cubic-bezier(.55, .085, .68, .53)",
"in-cubic": "cubic-bezier(.550, .055, .675, .19)",
"in-quart": "cubic-bezier(.895, .03, .685, .22)",
"in-back": "cubic-bezier(0.6,-0.28,0.74,0.05)",
"out-quad": "cubic-bezier(.25, .46, .45, .94)",
"out-cubic": "cubic-bezier(.215, .61, .355, 1)",
"out-quart": "cubic-bezier(.165, .84, .44, 1)",
"out-back": "cubic-bezier(0.18,0.89,0.32,1.27)",
"in-out-quad": "cubic-bezier(.455, .03, .515, .955)",
"in-out-cubic": "cubic-bezier(.645, .045, .355, 1)",
"in-out-quart": "cubic-bezier(.77, 0, .175, 1)",
"in-out-back": "cubic-bezier(0.68,-0.55,0.27,1.55)",
}),
motionDuration: (
theme: (path: keyof ThemeConfig) => Record<string, string>
) => ({
...theme("transitionDuration"),
1500: "1500ms",
2000: "2000ms",
DEFAULT: "750ms",
}),
motionDelay: (
theme: (path: keyof ThemeConfig) => Record<string, string>
) => ({
...theme("motionDuration"),
DEFAULT: "0ms",
}),
motionLoopCount: {
infinite: "infinite",
once: "1",
twice: "2",
},
};