diff --git a/packages/runtime-tests/integration/composables/use-screen-reader.test.tsx b/packages/runtime-tests/integration/composables/use-screen-reader.test.tsx
new file mode 100644
index 0000000..8daa820
--- /dev/null
+++ b/packages/runtime-tests/integration/composables/use-screen-reader.test.tsx
@@ -0,0 +1,14 @@
+import { defineComponent } from "vue";
+import { expect, test } from "vite-plus/test";
+import { render } from "@vue-tui/testing";
+import { Text, useIsScreenReaderEnabled } from "@vue-tui/runtime";
+
+test("useIsScreenReaderEnabled returns false by default", async () => {
+ let result = false;
+ const App = defineComponent(() => {
+ result = useIsScreenReaderEnabled();
+ return () => sr test;
+ });
+ await render(App);
+ expect(result).toBe(false);
+});
diff --git a/packages/runtime-tests/integration/public-api.test.ts b/packages/runtime-tests/integration/public-api.test.ts
index 3cef2ba..df721f1 100644
--- a/packages/runtime-tests/integration/public-api.test.ts
+++ b/packages/runtime-tests/integration/public-api.test.ts
@@ -19,6 +19,7 @@ test("public API exposes documented members", () => {
"useStderr",
"useTerminalSize",
"useCursor",
+ "useIsScreenReaderEnabled",
]) {
expect(api).toHaveProperty(k);
}
diff --git a/packages/runtime/src/composables/useIsScreenReaderEnabled.ts b/packages/runtime/src/composables/useIsScreenReaderEnabled.ts
new file mode 100644
index 0000000..ee5ad4c
--- /dev/null
+++ b/packages/runtime/src/composables/useIsScreenReaderEnabled.ts
@@ -0,0 +1,9 @@
+import { inject } from "vue";
+import { AppContextKey } from "../context.ts";
+
+export function useIsScreenReaderEnabled(): boolean {
+ const ctx = inject(AppContextKey);
+ if (!ctx)
+ throw new Error("useIsScreenReaderEnabled() must be called inside a vue-tui render tree");
+ return ctx.isScreenReaderEnabled;
+}
diff --git a/packages/runtime/src/context.ts b/packages/runtime/src/context.ts
index 69b6a5a..726dafa 100644
--- a/packages/runtime/src/context.ts
+++ b/packages/runtime/src/context.ts
@@ -13,6 +13,7 @@ export interface AppContext {
stdin: NodeJS.ReadStream;
debug: boolean;
interactive: boolean;
+ isScreenReaderEnabled: boolean;
isRawModeSupported: boolean;
setRawMode: (mode: boolean) => void;
writeToStdout: (data: string) => void;
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 27722a5..2d0bd75 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -18,6 +18,7 @@ export { useStdout } from "./composables/useStdout.ts";
export { useStderr } from "./composables/useStderr.ts";
export { useTerminalSize } from "./composables/useTerminalSize.ts";
export { useCursor } from "./composables/useCursor.ts";
+export { useIsScreenReaderEnabled } from "./composables/useIsScreenReaderEnabled.ts";
export {
useAnimation,
type AnimationOptions,
diff --git a/packages/runtime/src/render-to-string.ts b/packages/runtime/src/render-to-string.ts
index 119ffd4..ea32603 100644
--- a/packages/runtime/src/render-to-string.ts
+++ b/packages/runtime/src/render-to-string.ts
@@ -157,6 +157,7 @@ function createNoOpAppContext(): AppContext {
stdin: process.stdin,
debug: false,
interactive: false,
+ isScreenReaderEnabled: false,
isRawModeSupported: false,
setRawMode: () => {},
writeToStdout: () => {},
diff --git a/packages/runtime/src/render.ts b/packages/runtime/src/render.ts
index 7f9287b..6598fd6 100644
--- a/packages/runtime/src/render.ts
+++ b/packages/runtime/src/render.ts
@@ -75,6 +75,13 @@ export interface MountOptions {
* Ignored in debug mode (commits are immediate).
*/
maxFps?: number;
+ /**
+ * Enable screen reader mode. When enabled, the commit scheduler bypasses
+ * throttling (immediate commits) so every frame is flushed without delay.
+ *
+ * @default true when `process.env["INK_SCREEN_READER"] === "true"`, otherwise false
+ */
+ isScreenReaderEnabled?: boolean;
}
export interface TuiApp extends Omit, "mount"> {
@@ -215,6 +222,8 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
const rawMode = options.rawMode ?? true;
const onRender = options.onRender;
const maxFps = options.maxFps;
+ const isScreenReaderEnabled =
+ options.isScreenReaderEnabled ?? process.env["INK_SCREEN_READER"] === "true";
mountedDebug = debug;
// Interactive mode detection — matches Ink's logic:
@@ -298,6 +307,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
stdin,
debug,
interactive,
+ isScreenReaderEnabled,
isRawModeSupported: !!(stdin as { isTTY?: boolean }).isTTY,
setRawMode(mode: boolean) {
if (
@@ -391,8 +401,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
if (onRender) onRender({ renderTime: performance.now() - start });
}
- const schedulerOptions: { immediate: boolean; throttleMs?: number } = { immediate: debug };
- if (maxFps != null && !debug) {
+ const schedulerOptions: { immediate: boolean; throttleMs?: number } = {
+ immediate: debug || isScreenReaderEnabled,
+ };
+ if (maxFps != null && !debug && !isScreenReaderEnabled) {
schedulerOptions.throttleMs = Math.round(1000 / maxFps);
}
const scheduler = createCommitScheduler(commit, schedulerOptions);