Mouse Support

Mouse tracking, hover detection, click handling, and element hit-testing for Ink-compatible components in @visulima/tui/ink

Mouse Support

Provides mouse position tracking, hover detection, click handling, drag tracking, and element hit-testing for Ink-compatible components.

Ported from @zenobius/ink-mouse by Zeno Jiricek (Apache-2.0), rewritten to integrate with the ink input pipeline.

Features

  • Mouse position tracking across the terminal
  • Hover detection on specific elements
  • Click detection on specific elements
  • Drag tracking
  • Scroll wheel events
  • Element position and dimension tracking
  • Hit-test utility for custom intersection logic

Quick Start

Wrap your app with <MouseProvider> and use the hooks inside child components.

Note: You must also use useInput to prevent raw escape sequences from being printed to the terminal during mouse movements.

import { Box, Text, render, useInput } from "@visulima/tui/ink";
import { Fullscreen, MouseProvider, useMousePosition, useOnMouseClick, useOnMouseHover, useOnMouseState } from "@visulima/tui/ink";
import React, { useMemo, useRef, useState } from "react";
import type { ComponentProps, RefObject } from "react";
import type { DOMElement } from "@visulima/tui/ink";

function App() {
    return (
        <MouseProvider>
            <Demo />
        </MouseProvider>
    );
}

function Demo() {
    const position = useMousePosition();
    const [count, setCount] = useState(0);

    // Consume escape sequences so they don't print to the terminal
    useInput(() => {});

    return (
        <Box gap={1} flexDirection="column" width={40} height={10} borderStyle="round" padding={1}>
            <Box gap={1}>
                <Button label="Click me" onClick={() => setCount((c) => c + 1)} />
            </Box>
            <Box flexDirection="column" gap={1}>
                <Text>
                    Mouse: {position.x}, {position.y}
                </Text>
                <Text>Clicked: {count} times</Text>
            </Box>
        </Box>
    );
}

function Button({ label, onClick }: { label: string; onClick?: () => void }) {
    const ref = useRef<DOMElement | null>(null);
    const [hovering, setHovering] = useState(false);
    const [clicking, setClicking] = useState(false);

    useOnMouseClick(ref, (event) => {
        setClicking(event);
        if (event && onClick) {
            onClick();
        }
    });
    useOnMouseHover(ref, setHovering);

    const border = useMemo((): ComponentProps<typeof Box>["borderStyle"] => {
        if (clicking) return "double";
        if (hovering) return "singleDouble";
        return "single";
    }, [clicking, hovering]);

    return (
        <Box gap={1} paddingX={1} ref={ref} borderStyle={border}>
            <Text>{label}</Text>
        </Box>
    );
}

render(<App />);

Components

<MouseProvider>

Wraps the component tree to enable mouse tracking. Listens on ink's input pipeline for SGR mouse sequences and provides mouse state to all descendant hooks via context.

Mouse tracking ANSI codes are enabled on mount and disabled on unmount.

<MouseProvider>
    <YourApp />
</MouseProvider>

<Fullscreen>

A convenience component that fills the entire terminal. Uses useWindowSize to track terminal dimensions.

<MouseProvider>
    <Fullscreen>
        <YourApp />
    </Fullscreen>
</MouseProvider>

Hooks

All hooks below must be used inside a <MouseProvider>.

useMouseContext()

Returns the full mouse context object. Useful for accessing the internal event emitter directly.

const mouse = useMouseContext();
// mouse.position  - { x, y }
// mouse.button    - "left" | "middle" | "right" | null
// mouse.click     - "press" | "release" | null
// mouse.scroll    - "scrollup" | "scrolldown" | null
// mouse.drag      - "dragging" | null
// mouse.events    - EventEmitter (position, click, scroll, drag events)

useMousePosition()

Returns reactive mouse coordinates. Re-renders the component on every mouse move.

const { x, y } = useMousePosition();

useMouseAction()

Returns the most recent mouse action (click, scroll, or drag).

const action = useMouseAction();
// "press" | "release" | "scrollup" | "scrolldown" | "dragging" | null

useOnMouseClick(ref, onChange, options?)

Fires onChange(true, button) when the mouse clicks inside the element, onChange(false) otherwise. Supports left, middle, and right mouse buttons.

const ref = useRef<DOMElement | null>(null);

// Respond to any button
useOnMouseClick(ref, (isClicking, button) => {
    if (isClicking) {
        console.log(`${button} click!`);
    }
});

// Respond to right-click only
useOnMouseClick(
    ref,
    (isClicking) => {
        if (isClicking) showContextMenu();
    },
    { button: "right" },
);

return <Box ref={ref}>...</Box>;
ArgumentType
refRefObject<DOMElement | null>
onChange(clicking: boolean, button?: MouseButton) => void
options?{ button?: MouseButton }

When options.button is set, the hook only fires for that specific button.

useOnMouseHover(ref, onChange)

Fires onChange(true) when the mouse enters the element, onChange(false) when it leaves.

const ref = useRef<DOMElement | null>(null);

useOnMouseHover(ref, (isHovering) => {
    setHighlighted(isHovering);
});

return <Box ref={ref}>...</Box>;
ArgumentType
refRefObject<DOMElement | null>
onChange(hovering: boolean) => void

useOnMouseState(ref)

Combines click and hover detection into a single hook.

const ref = useRef<DOMElement | null>(null);
const { hovering, clicking } = useOnMouseState(ref);

return (
    <Box ref={ref} borderStyle={clicking ? "double" : hovering ? "bold" : "single"}>
        <Text>{hovering ? "Hovered!" : "Move mouse here"}</Text>
    </Box>
);

Returns { hovering: boolean, clicking: boolean }.

useElementPosition(ref, deps?)

Returns the absolute position of the element by walking the Yoga layout tree.

const ref = useRef<DOMElement | null>(null);
const { left, top } = useElementPosition(ref);

Terminal coordinates are 1-indexed. Pass optional deps array to recompute on specific changes.

useElementDimensions(ref, deps?)

Returns the computed dimensions of the element.

const ref = useRef<DOMElement | null>(null);
const { width, height } = useElementDimensions(ref);

Utilities

isIntersecting({ mouse, element })

Hit-test utility. Returns true if the mouse position falls within the element bounds.

import { isIntersecting } from "@visulima/tui/ink";

const hit = isIntersecting({
    mouse: { x: 10, y: 5 },
    element: { left: 8, top: 3, width: 20, height: 10 },
});
// true

Types

import type {
    MouseButton,
    MousePosition,
    MouseClickAction,
    MouseScrollAction,
    MouseDragAction,
    MouseAction,
    MouseContextShape,
    MouseEvents,
    SgrMouseEvent,
} from "@visulima/tui/ink";
TypeDefinition
MouseButton"left" | "middle" | "right"
MousePosition{ x: number; y: number }
MouseClickAction"press" | "release" | null
MouseScrollAction"scrollup" | "scrolldown" | null
MouseDragAction"dragging" | null
MouseActionMouseClickAction | MouseScrollAction
MouseContextShapeFull context value shape (includes button)
MouseEventsEventEmitter for position/click/scroll/drag
SgrMouseEventDiscriminated union from parseSgrMouse()

Architecture

The mouse module integrates with ink's existing input pipeline rather than attaching a separate stdin listener:

Terminal stdin
    |
    v
Ink App.tsx (reads stdin, parses escape sequences)
    |
    v
internal_eventEmitter.emit("input", sequence)
    |
    +---> useInput (keyboard)
    |
    +---> MouseProvider (detects SGR mouse sequences)
              |
              v
          EventEmitter (position/click/scroll/drag)
              |
              +---> useMousePosition()
              +---> useMouseAction()
              +---> useOnMouseClick(ref)
              +---> useOnMouseHover(ref)
              +---> useOnMouseState(ref)

This avoids competing stdin listeners and keeps mouse events in sync with keyboard input.

parseSgrMouse(input)

Low-level utility to parse an SGR 1006 mouse escape sequence. Returns a discriminated union or null.

import { parseSgrMouse } from "@visulima/tui/ink";

const event = parseSgrMouse("\x1b[<0;10;20M");
// { type: "click", button: "left", action: "press", x: 10, y: 20 }

Event types: "click", "drag", "move", "scroll". Modifier bits (shift, meta, ctrl) are automatically stripped from the button code.

Known Limitations

  • Overlapping elements both receive click/hover events (no z-order handling)
  • Drag examples are limited
  • Windows CMD.exe has limited ANSI support; Windows Terminal works
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues