-
- fade-up w/ dramatic easing
+
+
+
+ fade-up w/ dramatic easing
+
+
+
@@ -169,6 +250,7 @@ const animationSections = [
duration: 800,
offset: 50,
once: false,
+ timeUnits: "ms",
mirror: false,
});
diff --git a/apps/docs/src/content/docs/getting-started/installation.mdx b/apps/docs/src/content/docs/getting-started/installation.mdx
index e9ce254..844eccd 100644
--- a/apps/docs/src/content/docs/getting-started/installation.mdx
+++ b/apps/docs/src/content/docs/getting-started/installation.mdx
@@ -30,7 +30,7 @@ yarn add motion-on-scroll
- Motion-On-Scroll (MOS) depends only on the motion package for its `animate` helpers.
+ Motion-On-Scroll (MOS) depends only on the motion package for its `animate` helper.
## Minimal bootstrapping
@@ -86,7 +86,7 @@ That’s it! Every element declaring a `data-mos` value is now animated when it
### CSS file
-MOS ships with a small stylesheet that ensures elements are hidden upon initial page load. Import it from `motion-on-scroll/dist/mos.css` or copy the file into your own pipeline. The contents of the file are copied below for convenience.
+MOS ships with a small stylesheet that ensures elements are hidden upon initial page load. Import it from `motion-on-scroll/dist/mos.css` or copy the file into your own pipeline. The contents of the file are below for convenience.
```css
/* no pointer events until animation starts */
diff --git a/apps/docs/src/content/docs/reference/api.mdx b/apps/docs/src/content/docs/reference/api.mdx
index 90519e3..a74c48a 100644
--- a/apps/docs/src/content/docs/reference/api.mdx
+++ b/apps/docs/src/content/docs/reference/api.mdx
@@ -29,7 +29,7 @@ MOS.init({
*Type*: `number` (pixels)
*Default*: `120`
-Additional distance (in pixels) before an element is considered *in view* (applied via `rootMargin`).
+Additional distance (in pixels) before an element is considered *in view*.
#### duration
diff --git a/apps/docs/src/content/docs/reference/presets.mdx b/apps/docs/src/content/docs/reference/presets.mdx
index 5bd348d..8e82b6c 100644
--- a/apps/docs/src/content/docs/reference/presets.mdx
+++ b/apps/docs/src/content/docs/reference/presets.mdx
@@ -5,7 +5,7 @@ description: Animation presets shipped with Motion-On-Scroll and how to extend t
## Available presets
-All of the standard AOS presets are supported.
+All of the standard AOS presets are supported. You can see them in action on the [homepage](/)!
### Fades
diff --git a/packages/motion-on-scroll/README.md b/packages/motion-on-scroll/README.md
index ddd5fdc..57d5aa7 100644
--- a/packages/motion-on-scroll/README.md
+++ b/packages/motion-on-scroll/README.md
@@ -2,7 +2,7 @@
[](https://npmjs.org/package/motion-on-scroll) [](https://npmjs.org/package/motion-on-scroll) [](LICENSE) [](https://twitter.com/webreaper)
-## Note: this is currently in beta - there are still various items to be ironed out. Feel free to try it though :)
+## Note: this is currently in beta. Feel free to try it though :)
Effortless, AOS-compatible scroll animations powered by [Motion](https://motion.dev).
diff --git a/packages/motion-on-scroll/src/__tests__/index.spec.ts b/packages/motion-on-scroll/src/__tests__/index.spec.ts
new file mode 100644
index 0000000..f77fa85
--- /dev/null
+++ b/packages/motion-on-scroll/src/__tests__/index.spec.ts
@@ -0,0 +1,618 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { DEFAULT_OPTIONS } from "../helpers/constants.js";
+
+// Mock all dependencies
+vi.mock("../helpers/animations.js", () => ({
+ registerAnimation: vi.fn(),
+}));
+
+vi.mock("../helpers/easing.js", () => ({
+ registerEasing: vi.fn(),
+}));
+
+vi.mock("../helpers/elements.js", () => ({
+ clearAllElements: vi.fn(),
+ getMosElements: vi.fn(() => []),
+ prepareElements: vi.fn(),
+}));
+
+vi.mock("../helpers/keyframes.js", () => ({
+ registerKeyframes: vi.fn(),
+}));
+
+vi.mock("../helpers/observer.js", () => ({
+ startDomObserver: vi.fn(),
+}));
+
+vi.mock("../helpers/scroll-handler.js", () => ({
+ cleanupScrollHandler: vi.fn(),
+ ensureScrollHandlerActive: vi.fn(),
+ evaluateElementPositions: vi.fn(),
+ updateScrollHandlerDelays: vi.fn(),
+}));
+
+vi.mock("../helpers/utils.js", () => ({
+ debounce: vi.fn((fn) => fn),
+ isDisabled: vi.fn(() => false),
+ removeMosAttributes: vi.fn(),
+}));
+
+// Import mocked functions
+import { registerAnimation } from "../helpers/animations.js";
+import { registerEasing } from "../helpers/easing.js";
+import { clearAllElements, getMosElements, prepareElements } from "../helpers/elements.js";
+import { registerKeyframes } from "../helpers/keyframes.js";
+import { startDomObserver } from "../helpers/observer.js";
+import {
+ cleanupScrollHandler,
+ ensureScrollHandlerActive,
+ evaluateElementPositions,
+ updateScrollHandlerDelays,
+} from "../helpers/scroll-handler.js";
+import { debounce, isDisabled, removeMosAttributes } from "../helpers/utils.js";
+
+describe("index.ts - Main Entry Point", () => {
+ let mockElement1: HTMLElement;
+ let mockElement2: HTMLElement;
+ let MOS: any;
+ let init: any;
+ let refresh: any;
+ let refreshHard: any;
+ let handleLayoutChange: any;
+ let setupStartEventListener: any;
+
+ beforeEach(async () => {
+ // Reset all mocks
+ vi.clearAllMocks();
+
+ // Create mock DOM elements
+ mockElement1 = document.createElement("div");
+ mockElement1.setAttribute("data-mos", "fade");
+ mockElement1.setAttribute("id", "element1");
+
+ mockElement2 = document.createElement("div");
+ mockElement2.setAttribute("data-mos", "slide-up");
+ mockElement2.setAttribute("id", "element2");
+
+ // Mock getMosElements to return our test elements
+ vi.mocked(getMosElements).mockReturnValue([mockElement1, mockElement2]);
+
+ // Mock document.readyState since it's read-only
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "loading",
+ });
+
+ // Mock global MutationObserver
+ global.MutationObserver = vi.fn(() => ({
+ observe: vi.fn(),
+ disconnect: vi.fn(),
+ takeRecords: vi.fn(),
+ })) as any;
+
+ // Reset modules and re-import to get fresh state
+ vi.resetModules();
+ const indexModule = await import("../index.js");
+ MOS = indexModule.MOS;
+ init = indexModule.init;
+ refresh = indexModule.refresh;
+ refreshHard = indexModule.refreshHard;
+ handleLayoutChange = indexModule.handleLayoutChange;
+ setupStartEventListener = indexModule.setupStartEventListener;
+ });
+
+ describe("MOS object export", () => {
+ it("should export MOS object with all required methods", () => {
+ expect(MOS).toBeDefined();
+ expect(MOS.init).toBeDefined();
+ expect(MOS.refresh).toBeDefined();
+ expect(MOS.refreshHard).toBeDefined();
+ expect(MOS.registerKeyframes).toBeDefined();
+ expect(MOS.registerEasing).toBeDefined();
+ expect(MOS.registerAnimation).toBeDefined();
+ });
+
+ it("should have function exports matching MOS object", () => {
+ expect(typeof init).toBe("function");
+ expect(typeof refresh).toBe("function");
+ expect(typeof refreshHard).toBe("function");
+ });
+ });
+
+ describe("init()", () => {
+ it("should initialize with default options when no options provided", () => {
+ const result = init();
+
+ expect(getMosElements).toHaveBeenCalled();
+ expect(result).toEqual([mockElement1, mockElement2]);
+ });
+
+ it("should merge provided options with defaults", () => {
+ const customOptions = {
+ duration: 800,
+ easing: "ease-in-out",
+ delay: 100,
+ };
+
+ init(customOptions);
+
+ // Should still find elements
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should handle time unit conversion on first init", () => {
+ const optionsWithSeconds = {
+ timeUnits: "s" as const,
+ };
+
+ init(optionsWithSeconds);
+
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should handle global disable by cleaning up elements", () => {
+ vi.mocked(isDisabled).mockReturnValue(true);
+
+ const result = init();
+
+ // removeMosAttributes is called with forEach, so it gets (element, index, array)
+ expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [
+ mockElement1,
+ mockElement2,
+ ]);
+ expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [
+ mockElement1,
+ mockElement2,
+ ]);
+ expect(result).toEqual([]);
+ });
+
+ it("should start DOM observer when not disabled and MutationObserver exists", () => {
+ // Mock isDisabled to return false so we don't exit early
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ init({ disableMutationObserver: false });
+
+ expect(startDomObserver).toHaveBeenCalled();
+ });
+
+ it("should not start DOM observer when disabled", () => {
+ init({ disableMutationObserver: true });
+
+ expect(startDomObserver).not.toHaveBeenCalled();
+ });
+
+ it("should not start DOM observer when MutationObserver is not supported", () => {
+ // Remove MutationObserver
+ const originalMutationObserver = global.MutationObserver;
+ delete (global as any).MutationObserver;
+
+ init();
+
+ expect(startDomObserver).not.toHaveBeenCalled();
+
+ // Restore MutationObserver
+ global.MutationObserver = originalMutationObserver;
+ });
+
+ it("should refresh when called multiple times", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // First init
+ init();
+ expect(getMosElements).toHaveBeenCalledTimes(1);
+
+ // Second init should trigger refresh
+ init({ duration: 600 });
+ expect(getMosElements).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe("refresh()", () => {
+ it("should not activate library when shouldActivate is false and library is inactive", () => {
+ refresh(false);
+
+ // Should not call any scroll handler functions when library is inactive
+ expect(updateScrollHandlerDelays).not.toHaveBeenCalled();
+ expect(prepareElements).not.toHaveBeenCalled();
+ expect(ensureScrollHandlerActive).not.toHaveBeenCalled();
+ expect(evaluateElementPositions).not.toHaveBeenCalled();
+ });
+
+ it("should activate library when shouldActivate is true", () => {
+ refresh(true);
+
+ expect(updateScrollHandlerDelays).toHaveBeenCalled();
+ expect(getMosElements).toHaveBeenCalled();
+ expect(prepareElements).toHaveBeenCalledWith([mockElement1, mockElement2], DEFAULT_OPTIONS);
+ expect(ensureScrollHandlerActive).toHaveBeenCalled();
+ expect(evaluateElementPositions).toHaveBeenCalled();
+ });
+
+ it("should update scroll handler delays with library config", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // First init to set library as active
+ init({ throttleDelay: 150 });
+
+ // Call refresh with shouldActivate=true to ensure library is active
+ refresh(true);
+
+ expect(updateScrollHandlerDelays).toHaveBeenCalledWith(150);
+ });
+
+ it("should prepare elements with current library config", () => {
+ const customOptions = { duration: 800, delay: 200 };
+
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Init with custom options to set library config
+ init(customOptions);
+
+ // Call refresh with shouldActivate=true to ensure library is active
+ refresh(true);
+
+ expect(prepareElements).toHaveBeenCalledWith(
+ [mockElement1, mockElement2],
+ expect.objectContaining(customOptions),
+ );
+ });
+ });
+
+ describe("refreshHard()", () => {
+ it("should re-find elements and clear existing state", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Set up library as active first
+ init();
+
+ refreshHard();
+
+ expect(getMosElements).toHaveBeenCalledWith(true); // Force refresh
+ expect(clearAllElements).toHaveBeenCalled();
+ expect(cleanupScrollHandler).toHaveBeenCalled();
+ });
+
+ it("should handle global disable during hard refresh", () => {
+ vi.mocked(isDisabled).mockReturnValue(true);
+
+ refreshHard();
+
+ // removeMosAttributes is called with forEach, so it gets (element, index, array)
+ expect(removeMosAttributes).toHaveBeenCalledWith(mockElement1, 0, [
+ mockElement1,
+ mockElement2,
+ ]);
+ expect(removeMosAttributes).toHaveBeenCalledWith(mockElement2, 1, [
+ mockElement1,
+ mockElement2,
+ ]);
+
+ // When disabled, function returns early - cleanup functions are NOT called
+ expect(clearAllElements).not.toHaveBeenCalled();
+ expect(cleanupScrollHandler).not.toHaveBeenCalled();
+ });
+
+ it("should call refresh after cleanup when not disabled", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ refreshHard();
+
+ expect(clearAllElements).toHaveBeenCalled();
+ expect(cleanupScrollHandler).toHaveBeenCalled();
+
+ // refresh() is called, but updateScrollHandlerDelays only happens if library is active
+ // Since we haven't activated the library, updateScrollHandlerDelays won't be called
+ expect(getMosElements).toHaveBeenCalled(); // This should be called by refresh
+ });
+ });
+
+ describe("handleLayoutChange()", () => {
+ it("should evaluate element positions when library is active", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Activate library first
+ init();
+ refresh(true);
+
+ handleLayoutChange();
+
+ expect(evaluateElementPositions).toHaveBeenCalled();
+ });
+
+ it("should not evaluate positions when library is inactive", () => {
+ handleLayoutChange();
+
+ expect(evaluateElementPositions).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("setupStartEventListener()", () => {
+ beforeEach(() => {
+ // Mock addEventListener
+ vi.spyOn(window, "addEventListener");
+ vi.spyOn(document, "addEventListener");
+ });
+
+ it("should call refresh immediately when DOMContentLoaded already fired", async () => {
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "interactive",
+ });
+
+ // Re-import to get fresh module state with new readyState
+ vi.resetModules();
+ const { setupStartEventListener } = await import("../index.js");
+
+ setupStartEventListener();
+
+ // Should not add event listener since DOM is already ready
+ expect(document.addEventListener).not.toHaveBeenCalledWith(
+ "DOMContentLoaded",
+ expect.any(Function),
+ expect.any(Object),
+ );
+ });
+
+ it("should call refresh immediately when load already fired", async () => {
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "complete",
+ });
+
+ vi.resetModules();
+ const { setupStartEventListener } = await import("../index.js");
+
+ setupStartEventListener();
+
+ // Should not add event listener since page is already loaded
+ expect(window.addEventListener).not.toHaveBeenCalledWith(
+ "load",
+ expect.any(Function),
+ expect.any(Object),
+ );
+ });
+
+ it("should add window event listener for load event", () => {
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "loading",
+ });
+
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Initialize with load event
+ init({ startEvent: "load" });
+
+ expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function), {
+ once: true,
+ });
+ });
+
+ it("should add document event listener for DOMContentLoaded", () => {
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "loading",
+ });
+
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Initialize with DOMContentLoaded (default)
+ init({ startEvent: "DOMContentLoaded" });
+
+ expect(document.addEventListener).toHaveBeenCalledWith(
+ "DOMContentLoaded",
+ expect.any(Function),
+ { once: true },
+ );
+ });
+
+ it("should add document event listener for custom events", () => {
+ Object.defineProperty(document, "readyState", {
+ writable: true,
+ value: "loading",
+ });
+
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ init({ startEvent: "custom-event" });
+
+ expect(document.addEventListener).toHaveBeenCalledWith("custom-event", expect.any(Function), {
+ once: true,
+ });
+ });
+ });
+
+ describe("Layout change event listeners", () => {
+ beforeEach(() => {
+ vi.spyOn(window, "addEventListener");
+ });
+
+ it("should set up resize and orientation change listeners", () => {
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ init();
+
+ expect(window.addEventListener).toHaveBeenCalledWith("resize", expect.any(Function));
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ "orientationchange",
+ expect.any(Function),
+ );
+ });
+
+ it("should use debounced handler with configured delay", () => {
+ const customDebounceDelay = 200;
+
+ // Mock isDisabled to return false
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ init({ debounceDelay: customDebounceDelay });
+
+ expect(debounce).toHaveBeenCalledWith(expect.any(Function), customDebounceDelay);
+ });
+ });
+
+ describe("Registration functions", () => {
+ it("should expose registerKeyframes function", () => {
+ const mockKeyframes = { "custom-fade": { from: { opacity: 0 }, to: { opacity: 1 } } };
+
+ MOS.registerKeyframes(mockKeyframes);
+
+ expect(registerKeyframes).toHaveBeenCalledWith(mockKeyframes);
+ });
+
+ it("should expose registerEasing function", () => {
+ const mockEasing = { "custom-ease": "cubic-bezier(0.25, 0.46, 0.45, 0.94)" };
+
+ MOS.registerEasing(mockEasing);
+
+ expect(registerEasing).toHaveBeenCalledWith(mockEasing);
+ });
+
+ it("should expose registerAnimation function", () => {
+ const mockAnimation = {
+ name: "custom-animation",
+ factory: () => ({ opacity: [0, 1] }),
+ };
+
+ MOS.registerAnimation(mockAnimation);
+
+ expect(registerAnimation).toHaveBeenCalledWith(mockAnimation);
+ });
+ });
+
+ describe("Time units adjustment", () => {
+ it("should convert default duration from ms to seconds when timeUnits is 's'", () => {
+ const options = {
+ timeUnits: "s" as const,
+ // Don't specify duration - should use converted default
+ };
+
+ init(options);
+
+ // The conversion should happen internally
+ // Default duration (400ms) should become 0.4s
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should convert default delay from ms to seconds when timeUnits is 's'", () => {
+ const options = {
+ timeUnits: "s" as const,
+ // Don't specify delay - should use converted default
+ };
+
+ init(options);
+
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should not convert when timeUnits is 'ms'", () => {
+ const options = {
+ timeUnits: "ms" as const,
+ };
+
+ init(options);
+
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should not convert on subsequent inits", () => {
+ // First init with seconds
+ init({ timeUnits: "s" });
+
+ // Second init should not trigger conversion again
+ init({ timeUnits: "s" });
+
+ expect(getMosElements).toHaveBeenCalledTimes(2);
+ });
+
+ it("should not convert when explicit duration is provided", () => {
+ const options = {
+ timeUnits: "s" as const,
+ duration: 2, // Explicit duration should not be converted
+ };
+
+ init(options);
+
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should not convert when explicit delay is provided", () => {
+ const options = {
+ timeUnits: "s" as const,
+ delay: 1, // Explicit delay should not be converted
+ };
+
+ init(options);
+
+ expect(getMosElements).toHaveBeenCalled();
+ });
+
+ it("should convert default duration and delay when timeUnits is 's' and no explicit values provided", () => {
+ // Mock isDisabled to return false so init completes
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Mock prepareElements to capture the config that gets passed
+ let capturedConfig: any;
+ vi.mocked(prepareElements).mockImplementation((elements, config) => {
+ capturedConfig = config;
+ return []; // Return empty array to match MosElement[] return type
+ });
+
+ // Call init with timeUnits: "s" but no explicit duration or delay
+ init({ timeUnits: "s" });
+
+ // Trigger refresh to ensure prepareElements gets called with the config
+ refresh(true);
+
+ // Verify that the config passed to prepareElements has converted defaults
+ expect(capturedConfig).toBeDefined();
+ expect(capturedConfig.timeUnits).toBe("s");
+
+ // Default duration should be converted: 400ms -> 0.4s
+ expect(capturedConfig.duration).toBe(0.4);
+
+ // Default delay should be converted: 0ms -> 0s
+ expect(capturedConfig.delay).toBe(0);
+ });
+
+ it("should not convert defaults when explicit duration and delay are provided with timeUnits 's'", () => {
+ // Mock isDisabled to return false so init completes
+ vi.mocked(isDisabled).mockReturnValue(false);
+
+ // Mock prepareElements to capture the config that gets passed
+ let capturedConfig: any;
+ vi.mocked(prepareElements).mockImplementation((elements, config) => {
+ capturedConfig = config;
+ return []; // Return empty array to match MosElement[] return type
+ });
+
+ // Call init with timeUnits: "s" AND explicit duration/delay
+ init({
+ timeUnits: "s",
+ duration: 1.5, // Explicit value should not be converted
+ delay: 0.2, // Explicit value should not be converted
+ });
+
+ // Trigger refresh to ensure prepareElements gets called with the config
+ refresh(true);
+
+ // Verify that the explicit values are preserved (not converted)
+ expect(capturedConfig).toBeDefined();
+ expect(capturedConfig.timeUnits).toBe("s");
+ expect(capturedConfig.duration).toBe(1.5); // Should remain as provided
+ expect(capturedConfig.delay).toBe(0.2); // Should remain as provided
+ });
+ });
+});
diff --git a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts
index 9f6be70..9770435 100644
--- a/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts
+++ b/packages/motion-on-scroll/src/__tests__/scroll-handler.spec.ts
@@ -287,6 +287,37 @@ describe("scroll-handler.ts", () => {
// Should trigger play since scrollY (150) >= position.in (100)
expect(play).toHaveBeenCalledWith(mockMosElement1);
});
+
+ it("should preserve animated element states during resize (flicker fix)", () => {
+ // Setup: Create elements with different animation states
+ const animatedElement = {
+ ...mockMosElement1,
+ animated: true, // Already animated
+ };
+ const nonAnimatedElement = {
+ ...mockMosElement2,
+ animated: false, // Not yet animated
+ };
+
+ vi.mocked(getPreparedElements).mockReturnValue([animatedElement, nonAnimatedElement]);
+ vi.mocked(isElementAboveViewport).mockReturnValue(false);
+
+ evaluateElementPositions();
+
+ // Should recalculate positions for both elements
+ expect(getPositionIn).toHaveBeenCalledWith(animatedElement.element, animatedElement.options);
+ expect(getPositionIn).toHaveBeenCalledWith(
+ nonAnimatedElement.element,
+ nonAnimatedElement.options,
+ );
+
+ // Should NOT reset initial state for already-animated element (prevents flicker)
+ expect(setInitialState).not.toHaveBeenCalledWith(animatedElement);
+ expect(setFinalState).not.toHaveBeenCalledWith(animatedElement);
+
+ // Should still set initial state for non-animated element
+ expect(setInitialState).toHaveBeenCalledWith(nonAnimatedElement);
+ });
});
describe("scroll event processing", () => {
diff --git a/packages/motion-on-scroll/src/helpers/scroll-handler.ts b/packages/motion-on-scroll/src/helpers/scroll-handler.ts
index 4d93671..fde0e8d 100644
--- a/packages/motion-on-scroll/src/helpers/scroll-handler.ts
+++ b/packages/motion-on-scroll/src/helpers/scroll-handler.ts
@@ -131,11 +131,17 @@ function setElementInitialState(elementData: MosElement): void {
/**
* Refreshes all tracked elements by recalculating positions and states
* Called when the library needs to update after configuration changes
+ * Preserves current animation states to prevent flicker during resize
*/
export function evaluateElementPositions(): void {
getPreparedElements().forEach((elementData) => {
calculateElementTriggerPositions(elementData);
- setElementInitialState(elementData);
+
+ // Only reset initial state for elements that haven't been animated yet
+ // This prevents flicker during resize for already-animated elements
+ if (!elementData.animated) {
+ setElementInitialState(elementData);
+ }
});
// Process current scroll position to animate elements already in viewport
diff --git a/packages/motion-on-scroll/src/index.ts b/packages/motion-on-scroll/src/index.ts
index 1cf9d38..f5bf45b 100644
--- a/packages/motion-on-scroll/src/index.ts
+++ b/packages/motion-on-scroll/src/index.ts
@@ -42,16 +42,16 @@ let isLibraryActive = false;
* Only applied on first initialization when timeUnits is set to "s"
* @param config - The configuration object to potentially modify
*/
-function adjustTimeUnitsOnFirstInit(config: MosOptions): void {
+function adjustTimeUnitsOnFirstInit(config: MosOptions, newOptions: PartialMosOptions): void {
if (isLibraryActive || config.timeUnits !== "s") return;
// Convert default duration from ms to seconds if not explicitly set
- if (config.duration == null) {
+ if (newOptions.duration == null) {
config.duration = DEFAULT_OPTIONS.duration / 1000;
}
// Convert default delay from ms to seconds if not explicitly set
- if (config.delay == null) {
+ if (newOptions.delay == null) {
config.delay = DEFAULT_OPTIONS.delay / 1000;
}
}
@@ -116,7 +116,7 @@ function init(options: PartialMosOptions = {}): HTMLElement[] {
libraryConfig = { ...DEFAULT_OPTIONS, ...options };
// Handle time unit conversion on first initialization
- adjustTimeUnitsOnFirstInit(libraryConfig);
+ adjustTimeUnitsOnFirstInit(libraryConfig, options);
// If already initialized, just refresh with new options
if (isLibraryActive) {