diff --git a/plugins/inline-vis/app.test.tsx b/plugins/inline-vis/app.test.tsx
index 9dd40b01bc..567907ee86 100644
--- a/plugins/inline-vis/app.test.tsx
+++ b/plugins/inline-vis/app.test.tsx
@@ -63,7 +63,9 @@ describe("InlineVisDirective", () => {
       },
     );
 
-    await slot.findByText(/Loading visualization charts\/demo file\.html/i);
+    await slot.findByRole("status", {
+      name: "Loading visualization charts/demo file.html",
+    });
 
     const iframe = await waitFor(() => {
       const el = slot.container.querySelector("iframe");
@@ -116,6 +118,103 @@ describe("InlineVisDirective", () => {
     expect(iframe.style.height).toBe("480px");
   });
 
+  it("renders a complete fixed skeleton with shimmer-only motion", async () => {
+    let resolvePreview = (_result: { file: string }) => {};
+    const pendingPreview = new Promise<{ file: string }>((resolve) => {
+      resolvePreview = resolve;
+    });
+    const openWorkspaceFile = vi.fn(() => true);
+    const slot = renderSlot(
+      app.messageDirectives[0]!,
+      {
+        attributes: { file: "demo.html", height: "480" },
+        source: '::inline-vis{file="demo.html" height="480"}',
+        message,
+        openWorkspaceFile,
+      },
+      {
+        rpc: {
+          prepareHtmlPreview: () => pendingPreview,
+        },
+      },
+    );
+
+    const loadingSkeleton = await slot.findByRole("status", {
+      name: "Loading visualization demo.html",
+    });
+    expect(loadingSkeleton.style.height).toBe("480px");
+    const skeletonSteps = Array.from(
+      loadingSkeleton.querySelectorAll("[data-inline-vis-skeleton-step]"),
+    );
+    expect(
+      skeletonSteps.map((step) =>
+        step.getAttribute("data-inline-vis-skeleton-step"),
+      ),
+    ).toEqual([
+      "region-1",
+      "region-2",
+      "region-3",
+      "region-4",
+      "region-5",
+      "region-6",
+    ]);
+    for (const step of skeletonSteps) {
+      expect(
+        Array.from(step.classList).some((className) =>
+          className.startsWith("delay-"),
+        ),
+      ).toBe(false);
+      expect(step.classList.contains("animate-in")).toBe(false);
+      expect(step.classList.contains("fade-in-0")).toBe(false);
+      expect(step.classList.contains("slide-in-from-bottom-1")).toBe(false);
+      expect(step.classList.contains("fill-mode-backwards")).toBe(false);
+    }
+    const shimmerElements = Array.from(
+      loadingSkeleton.querySelectorAll("[data-inline-vis-skeleton-shimmer]"),
+    );
+    expect(shimmerElements).toHaveLength(6);
+    for (const shimmerElement of shimmerElements) {
+      expect(shimmerElement.classList.contains("animate-shine-icon")).toBe(
+        true,
+      );
+    }
+    expect(loadingSkeleton.querySelectorAll(".animate-pulse")).toHaveLength(0);
+    expect(
+      loadingSkeleton.querySelector("[data-inline-vis-skeleton-canvas]"),
+    ).not.toBeNull();
+    expect(
+      loadingSkeleton.querySelector("[data-inline-vis-skeleton-plot]"),
+    ).toBeNull();
+    expect(
+      skeletonSteps.some((step) =>
+        step.getAttribute("data-inline-vis-skeleton-step")?.includes("bar"),
+      ),
+    ).toBe(false);
+    expect(
+      loadingSkeleton.querySelector(".min-h-0.w-full.flex-1.animate-pulse"),
+    ).toBeNull();
+    const loadingCard = loadingSkeleton.parentElement;
+    const actionSpacer = loadingCard?.firstElementChild?.lastElementChild;
+    expect(actionSpacer?.getAttribute("aria-hidden")).toBe("true");
+    expect(actionSpacer?.classList.contains("size-5")).toBe(true);
+
+    resolvePreview({ file: "demo.html" });
+
+    const iframe = await waitFor(() => {
+      const el = slot.container.querySelector("iframe");
+      if (!(el instanceof HTMLIFrameElement)) {
+        throw new Error("Expected the inline visualization iframe to render");
+      }
+      return el;
+    });
+    expect(iframe.style.height).toBe("480px");
+    expect(
+      slot.queryByRole("status", {
+        name: "Loading visualization demo.html",
+      }),
+    ).toBeNull();
+  });
+
   it("rejects an invalid height without calling rpc", async () => {
     const slot = renderSlot(
       app.messageDirectives[0]!,
diff --git a/plugins/inline-vis/app.tsx b/plugins/inline-vis/app.tsx
index 750773970c..7cb3de1642 100644
--- a/plugins/inline-vis/app.tsx
+++ b/plugins/inline-vis/app.tsx
@@ -4,6 +4,7 @@
 // resources work inside an opaque origin that cannot access the host page.
 import { useEffect, useState } from "react";
 import { Icon } from "@bb/shared-ui/icon";
+import { Skeleton } from "@bb/shared-ui/skeleton";
 import {
   definePluginApp,
   useRpc,
@@ -21,6 +22,14 @@ type LoadState =
 const DEFAULT_HEIGHT_PX = 224;
 const MIN_HEIGHT_PX = 120;
 const MAX_HEIGHT_PX = 1_200;
+const SKELETON_REGIONS = [
+  { id: "region-1", layoutClass: "col-span-7 row-span-2" },
+  { id: "region-2", layoutClass: "col-span-5" },
+  { id: "region-3", layoutClass: "col-span-5" },
+  { id: "region-4", layoutClass: "col-span-4 row-span-2" },
+  { id: "region-5", layoutClass: "col-span-5 row-span-2" },
+  { id: "region-6", layoutClass: "col-span-3 row-span-2" },
+] as const;
 
 interface PrepareHtmlPreviewRpcResult {
   file: string;
@@ -153,10 +162,46 @@ function InlineVisDirective({
   if (state.status === "loading") {
     return (
       <div
-        className="my-2 rounded-md border border-border bg-muted px-3 py-2 text-sm text-muted-foreground"
+        className="my-2 overflow-hidden rounded-lg border border-border bg-background"
         aria-busy="true"
       >
-        Loading visualization {state.file}…
+        <div className="flex items-center gap-2 border-b border-border px-3 py-1.5 text-xs text-muted-foreground">
+          <div className="flex min-w-0 flex-1 items-center gap-2">
+            <span className="shrink-0 font-semibold">inline-vis</span>
+            <span className="truncate opacity-70">{state.file}</span>
+          </div>
+          {openWorkspaceFile === null ? null : (
+            <span aria-hidden className="size-5 shrink-0" />
+          )}
+        </div>
+        <div
+          style={{ height: previewHeight ?? DEFAULT_HEIGHT_PX }}
+          className="w-full bg-background p-4"
+          role="status"
+          aria-label={`Loading visualization ${state.file}`}
+        >
+          <div
+            aria-hidden
+            className="grid size-full grid-cols-12 grid-rows-4 gap-3"
+            data-inline-vis-skeleton-canvas
+          >
+            {SKELETON_REGIONS.map(({ id, layoutClass }) => (
+              <div
+                key={id}
+                className={`${layoutClass} min-h-0 min-w-0`}
+                data-inline-vis-skeleton-step={id}
+              >
+                <Skeleton className="relative size-full overflow-hidden rounded-md animate-none">
+                  <span
+                    aria-hidden
+                    className="absolute inset-0 animate-shine-icon bg-foreground/10"
+                    data-inline-vis-skeleton-shimmer
+                  />
+                </Skeleton>
+              </div>
+            ))}
+          </div>
+        </div>
       </div>
     );
   }
