diff --git a/examples/map-server/mcp-app.html b/examples/map-server/mcp-app.html
index 473eb50f6..acb9f384f 100644
--- a/examples/map-server/mcp-app.html
+++ b/examples/map-server/mcp-app.html
@@ -1,72 +1,83 @@
-
+
-
-
-
+
+
+
CesiumJS Globe
-
-
+
+
Loading globe...
-
+
diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts
index 1475d81e7..f70cdb5fe 100644
--- a/examples/map-server/src/mcp-app.ts
+++ b/examples/map-server/src/mcp-app.ts
@@ -397,6 +397,17 @@ async function initCesium(): Promise {
// CesiumJS sets image-rendering: pixelated by default which looks bad on scaled displays
// Setting to "auto" allows the browser to apply smooth interpolation
cesiumViewer.canvas.style.imageRendering = "auto";
+ // Prevent touch events from propagating to the parent scroll view.
+ // CesiumJS uses pointer events internally, which don't suppress native
+ // scroll gesture recognition on touch devices. Explicit non-passive touch
+ // listeners with preventDefault() are needed.
+ for (const eventName of ["touchstart", "touchmove"] as const) {
+ cesiumViewer.canvas.addEventListener(
+ eventName,
+ (e: TouchEvent) => e.preventDefault(),
+ { passive: false },
+ );
+ }
// Note: DO NOT set resolutionScale = devicePixelRatio here!
// When useBrowserRecommendedResolution: false, Cesium already uses devicePixelRatio.
// Setting resolutionScale = devicePixelRatio would double the scaling (e.g., 2x2=4x on Retina)
diff --git a/examples/shadertoy-server/src/mcp-app.css b/examples/shadertoy-server/src/mcp-app.css
index 0449ea5ea..0d5b6f114 100644
--- a/examples/shadertoy-server/src/mcp-app.css
+++ b/examples/shadertoy-server/src/mcp-app.css
@@ -1,4 +1,5 @@
-html, body {
+html,
+body {
margin: 0;
width: 100%;
height: 100%;
@@ -23,6 +24,7 @@ html, body {
width: 100%;
height: 100%;
display: block;
+ touch-action: none;
}
#canvas.hidden {
@@ -68,7 +70,9 @@ html, body {
display: none; /* Hidden by default, shown when fullscreen available */
align-items: center;
justify-content: center;
- transition: background 0.2s, opacity 0.2s;
+ transition:
+ background 0.2s,
+ opacity 0.2s;
opacity: 0; /* Initially invisible, shown on View hover */
z-index: 100;
}
diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx
index 235931641..bf122166c 100644
--- a/examples/threejs-server/src/threejs-app.tsx
+++ b/examples/threejs-server/src/threejs-app.tsx
@@ -85,6 +85,7 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) {
display: "flex",
flexDirection: "column",
overflow: "hidden",
+ touchAction: "none",
background:
"linear-gradient(135deg, var(--color-background-secondary, light-dark(#f0f0f5, #2a2a3c)) 0%, var(--color-background-tertiary, light-dark(#e5e5ed, #1e1e2e)) 100%)",
}}
@@ -275,6 +276,14 @@ export default function ThreeJSApp({
useEffect(() => {
if (!code || !canvasRef.current || !containerRef.current) return;
+ // Prevent touch events from propagating to the parent scroll view.
+ // Three.js OrbitControls uses pointer events, which don't suppress native
+ // scroll gesture recognition on touch devices.
+ const canvas = canvasRef.current;
+ const preventDefault = (e: TouchEvent) => e.preventDefault();
+ canvas.addEventListener("touchstart", preventDefault, { passive: false });
+ canvas.addEventListener("touchmove", preventDefault, { passive: false });
+
// Cleanup previous animation
animControllerRef.current?.cleanup();
animControllerRef.current = createAnimationController();
@@ -289,7 +298,11 @@ export default function ThreeJSApp({
animControllerRef.current.visibilityAwareRAF,
).catch((e) => setError(e instanceof Error ? e.message : "Unknown error"));
- return () => animControllerRef.current?.cleanup();
+ return () => {
+ canvas.removeEventListener("touchstart", preventDefault);
+ canvas.removeEventListener("touchmove", preventDefault);
+ animControllerRef.current?.cleanup();
+ };
}, [code, height]);
if (isStreaming || !code) {
@@ -314,6 +327,7 @@ export default function ThreeJSApp({
height,
borderRadius: "var(--border-radius-lg, 8px)",
display: "block",
+ touchAction: "none",
}}
/>
{error && Error: {error}
}
diff --git a/examples/wiki-explorer-server/src/mcp-app.css b/examples/wiki-explorer-server/src/mcp-app.css
index 8f6f0246e..ea13ac2ac 100644
--- a/examples/wiki-explorer-server/src/mcp-app.css
+++ b/examples/wiki-explorer-server/src/mcp-app.css
@@ -1,6 +1,7 @@
#graph {
width: 100%;
height: 100vh;
+ touch-action: none;
}
#popup {
diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts
index 7d95cb8d6..69bec05d4 100644
--- a/examples/wiki-explorer-server/src/mcp-app.ts
+++ b/examples/wiki-explorer-server/src/mcp-app.ts
@@ -111,6 +111,18 @@ const graph = new ForceGraph(container)
})
.graphData(graphData);
+// Prevent touch events from propagating to the parent scroll view.
+// force-graph uses pointer events, which don't suppress native scroll gesture
+// recognition on touch devices.
+const graphCanvas = container.querySelector("canvas");
+if (graphCanvas) {
+ for (const eventName of ["touchstart", "touchmove"] as const) {
+ graphCanvas.addEventListener(eventName, (e) => e.preventDefault(), {
+ passive: false,
+ });
+ }
+}
+
// Handle window resize
function handleResize() {
const { width, height } = container.getBoundingClientRect();