diff --git a/.changeset/strong-crews-fry.md b/.changeset/strong-crews-fry.md
new file mode 100644
index 000000000..0517b4ad1
--- /dev/null
+++ b/.changeset/strong-crews-fry.md
@@ -0,0 +1,5 @@
+---
+"@ensembleui/react-runtime": patch
+---
+
+enable widget's OnLoadAction to handle storage-bound input bindings and prevent premature rendering
diff --git a/packages/runtime/src/runtime/__tests__/customWidget.test.tsx b/packages/runtime/src/runtime/__tests__/customWidget.test.tsx
index 7fa31a73e..500092497 100644
--- a/packages/runtime/src/runtime/__tests__/customWidget.test.tsx
+++ b/packages/runtime/src/runtime/__tests__/customWidget.test.tsx
@@ -47,6 +47,43 @@ describe("Custom Widget", () => {
         },
       }),
     );
+
+    // register a widget that tests storage timing in onLoad
+    WidgetRegistry.register(
+      "StorageTestWidget",
+      createCustomWidget({
+        name: "StorageTestWidget",
+        inputs: ["userFilters"],
+        onLoad: {
+          executeCode: `
+            console.log('userFilters in onLoad:', userFilters);
+            console.log('userFilters type:', typeof userFilters);
+            console.log('userFilters JSON:', JSON.stringify(userFilters));
+          `,
+        },
+        body: {
+          name: "Text",
+          properties: {
+            // eslint-disable-next-line no-template-curly-in-string
+            text: "UserFilters: ${JSON.stringify(userFilters)}",
+            id: "userFiltersText",
+          },
+        },
+      }),
+    );
+  });
+
+  beforeEach(() => {
+    sessionStorage.setItem(
+      "ensemble.storage",
+      JSON.stringify({
+        userFilters: { status: "active", priority: "high" },
+      }),
+    );
+  });
+
+  afterEach(() => {
+    sessionStorage.clear();
   });
 
   it("renders custom widget with unspecified inputs", async () => {
@@ -126,4 +163,109 @@ describe("Custom Widget", () => {
       expect(screen.queryByText("goodbye")).toBeNull();
     });
   });
+
+  it("onLoad action has access to widget inputs bound to storage", async () => {
+    const logSpy = jest.spyOn(console, "log");
+
+    render(
+      ,
+      {
+        wrapper: BrowserRouter,
+      },
+    );
+
+    // wait for the component to render and onLoad to execute
+    await waitFor(() => {
+      // verify console logs show userFilters was correctly evaluated
+      expect(logSpy).toHaveBeenCalledWith("userFilters in onLoad:", {
+        status: "active",
+        priority: "high",
+      });
+      expect(logSpy).toHaveBeenCalledWith("userFilters type:", "object");
+      expect(logSpy).toHaveBeenCalledWith(
+        "userFilters JSON:",
+        '{"status":"active","priority":"high"}',
+      );
+
+      const userFiltersText = screen.getByTestId("userFiltersText");
+
+      // verify that userFilters (widget input) got the correct storage value
+      expect(userFiltersText).toHaveTextContent(
+        'UserFilters: {"status":"active","priority":"high"}',
+      );
+    });
+  });
+
+  it("onLoad action executes after widget inputs are properly evaluated", async () => {
+    const logSpy = jest.spyOn(console, "log");
+
+    // set storage data with different key to test the exact user scenario
+    sessionStorage.setItem(
+      "ensemble.storage",
+      JSON.stringify({
+        userFilters: { category: "work", completed: false },
+      }),
+    );
+
+    render(
+      ,
+      {
+        wrapper: BrowserRouter,
+      },
+    );
+
+    // initially, the widget content should NOT be visible because onLoad hasn't completed yet
+    expect(screen.queryByTestId("userFiltersText")).not.toBeInTheDocument();
+
+    // wait for storage hydration and onLoad execution
+    await waitFor(() => {
+      // verify console logs show userFilters was correctly evaluated with different data
+      expect(logSpy).toHaveBeenCalledWith("userFilters in onLoad:", {
+        category: "work",
+        completed: false,
+      });
+      expect(logSpy).toHaveBeenCalledWith("userFilters type:", "object");
+      expect(logSpy).toHaveBeenCalledWith(
+        "userFilters JSON:",
+        '{"category":"work","completed":false}',
+      );
+
+      const userFiltersText = screen.getByTestId("userFiltersText");
+
+      // widget input should have the correct storage data
+      expect(userFiltersText).toHaveTextContent(
+        'UserFilters: {"category":"work","completed":false}',
+      );
+    });
+  });
 });
diff --git a/packages/runtime/src/runtime/customWidget.tsx b/packages/runtime/src/runtime/customWidget.tsx
index 7eecdd2c1..6dafbad01 100644
--- a/packages/runtime/src/runtime/customWidget.tsx
+++ b/packages/runtime/src/runtime/customWidget.tsx
@@ -9,6 +9,7 @@ import type {
   EnsembleAction,
 } from "@ensembleui/react-framework";
 import React, { useEffect, useState } from "react";
+import { isEmpty, some, includes } from "lodash-es";
 import { EnsembleRuntime } from "./runtime";
 // FIXME: refactor
 // eslint-disable-next-line import/no-cycle
@@ -40,13 +41,18 @@ export const createCustomWidget = (
     return (
       
         
-          
+          
             {EnsembleRuntime.render([widget.body])}
           
         
       
     );
   };
+
   return CustomWidget;
 };
 
@@ -54,12 +60,34 @@ const OnLoadAction: React.FC<
   React.PropsWithChildren<{
     action?: EnsembleAction;
     context: { [key: string]: unknown };
+    rawInputs: { [key: string]: unknown };
   }>
-> = ({ action, children, context }) => {
+> = ({ action, children, context, rawInputs }) => {
   const onLoadAction = useEnsembleAction(action);
   const [isComplete, setIsComplete] = useState(false);
+  // check if any inputs are bound to storage
+  const [areInputBindingsReady, setAreInputBindingsReady] = useState(
+    isEmpty(rawInputs) ||
+      !some(
+        rawInputs,
+        (input) =>
+          typeof input === "string" && includes(input, "ensemble.storage.get"),
+      ),
+  );
+
+  // wait for binding evaluation to complete on next tick
+  useEffect(() => {
+    if (areInputBindingsReady) {
+      return;
+    }
+    const timer = setTimeout(() => {
+      setAreInputBindingsReady(true);
+    }, 0);
+    return () => clearTimeout(timer);
+  }, [areInputBindingsReady]);
+
   useEffect(() => {
-    if (!onLoadAction?.callback || isComplete) {
+    if (!onLoadAction?.callback || isComplete || !areInputBindingsReady) {
       return;
     }
     try {
@@ -69,7 +97,14 @@ const OnLoadAction: React.FC<
     } finally {
       setIsComplete(true);
     }
-  }, [context, isComplete, onLoadAction?.callback]);
+  }, [context, isComplete, areInputBindingsReady, onLoadAction?.callback]);
+
+  // don't render children until onLoad completes to prevent flash of unwanted content
+  // this ensures that if onLoad sets initial state (like hiding/showing elements),
+  // users won't see a brief flash of the default state before the onLoad logic runs
+  if (!isComplete && action) {
+    return null;
+  }
 
   return <>{children}>;
 };