diff --git a/packages/runtime-tests/integration/composables/use-cursor.test.tsx b/packages/runtime-tests/integration/composables/use-cursor.test.tsx
new file mode 100644
index 0000000..826d4a4
--- /dev/null
+++ b/packages/runtime-tests/integration/composables/use-cursor.test.tsx
@@ -0,0 +1,56 @@
+import { defineComponent, nextTick, shallowRef } from "vue";
+import { describe, expect, test } from "vite-plus/test";
+import { render } from "@vue-tui/testing";
+import { Text, useCursor } from "@vue-tui/runtime";
+
+describe("useCursor", () => {
+ test("setCursorPosition updates cursor state", async () => {
+ const App = defineComponent(() => {
+ const { setCursorPosition } = useCursor();
+ setCursorPosition({ x: 5, y: 3 });
+ return () => cursor test;
+ });
+ const { lastFrame } = await render(App);
+ expect(lastFrame()).toContain("cursor test");
+ });
+
+ test("setCursorPosition can be updated reactively", async () => {
+ const pos = shallowRef<{ x: number; y: number } | undefined>({ x: 0, y: 0 });
+ const App = defineComponent(() => {
+ const { setCursorPosition } = useCursor();
+ setCursorPosition(pos.value);
+ return () => reactive;
+ });
+ const { lastFrame } = await render(App);
+ expect(lastFrame()).toContain("reactive");
+
+ // Update position — should not crash
+ pos.value = { x: 10, y: 5 };
+ await nextTick();
+ });
+
+ test("setCursorPosition accepts undefined to hide cursor", async () => {
+ const App = defineComponent(() => {
+ const { setCursorPosition } = useCursor();
+ setCursorPosition({ x: 1, y: 1 });
+ setCursorPosition(undefined);
+ return () => hidden;
+ });
+ const { lastFrame } = await render(App);
+ expect(lastFrame()).toContain("hidden");
+ });
+
+ test("cursor is cleared on unmount", async () => {
+ const App = defineComponent(() => {
+ useCursor();
+ return () => cursor;
+ });
+ const { unmount } = await render(App);
+ unmount();
+ // No crash = success (cursor position cleared via onScopeDispose)
+ });
+
+ test("throws when called outside render tree", () => {
+ expect(() => useCursor()).toThrow("useCursor() must be called inside a vue-tui render tree");
+ });
+});
diff --git a/packages/runtime-tests/integration/public-api.test.ts b/packages/runtime-tests/integration/public-api.test.ts
index 4588695..3cef2ba 100644
--- a/packages/runtime-tests/integration/public-api.test.ts
+++ b/packages/runtime-tests/integration/public-api.test.ts
@@ -18,6 +18,7 @@ test("public API exposes documented members", () => {
"useStdout",
"useStderr",
"useTerminalSize",
+ "useCursor",
]) {
expect(api).toHaveProperty(k);
}
diff --git a/packages/runtime/src/composables/useCursor.ts b/packages/runtime/src/composables/useCursor.ts
new file mode 100644
index 0000000..2bd5d3a
--- /dev/null
+++ b/packages/runtime/src/composables/useCursor.ts
@@ -0,0 +1,37 @@
+import { inject, shallowRef, watch, onScopeDispose } from "vue";
+import { AppContextKey } from "../context.ts";
+
+/**
+ * Returns `setCursorPosition` so a component can control the terminal cursor.
+ *
+ * Setting a position makes the cursor visible at the given coordinates
+ * (relative to the output origin). Pass `undefined` to hide the cursor.
+ * The cursor position is automatically cleared when the component unmounts.
+ */
+export function useCursor() {
+ const ctx = inject(AppContextKey);
+ if (!ctx) throw new Error("useCursor() must be called inside a vue-tui render tree");
+
+ const positionRef = shallowRef<{ x: number; y: number } | undefined>(undefined);
+
+ function setCursorPosition(position: { x: number; y: number } | undefined) {
+ positionRef.value = position;
+ }
+
+ // Propagate cursor position to app context synchronously so it is
+ // available to restoreLastOutput() after the next render commit.
+ watch(
+ positionRef,
+ (pos) => {
+ ctx.setCursorPosition(pos);
+ },
+ { flush: "sync" },
+ );
+
+ // On unmount, clear cursor position so the cursor is hidden.
+ onScopeDispose(() => {
+ ctx.setCursorPosition(undefined);
+ });
+
+ return { setCursorPosition };
+}
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts
index 84c17b3..a830aff 100644
--- a/packages/runtime/src/index.ts
+++ b/packages/runtime/src/index.ts
@@ -17,6 +17,7 @@ export { useStdin } from "./composables/useStdin.ts";
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 {
useBoxMetrics,
measureElement,