Using Reactive

The Reactive module adds position tracking to boxes. Annotate any box with a string ID, then after layout you can query where it ended up: row, column, width, and height. This is useful for cursor navigation, hit-testing, and targeted updates in interactive terminal applications.

Making a box reactive

Use Reactive.makeReactive to tag a box with an ID:

import * as Box from "effect-boxes/Box";
import * as Reactive from "effect-boxes/Reactive";
 
const layout = Box.vcat(
  [
    Reactive.makeReactive(Box.text("Header"), "header"),
    Box.text("Content"),
    Reactive.makeReactive(Box.text("Footer"), "footer"),
  ],
  Box.left
);
Header
Content
Footer

The layout renders identically. Reactive annotations don't affect appearance, they only enable position queries after composition.

Querying positions

Call Reactive.getPositions on any composed box to get a HashMap of IDs to their calculated positions:

import { HashMap } from "effect";
import * as Reactive from "effect-boxes/Reactive";
 
const positions = Reactive.getPositions(layout);
 
const headerPos = HashMap.get(positions, "header");
// Some({ row: 0, col: 0, rows: 1, cols: 6 })
 
const footerPos = HashMap.get(positions, "footer");
// Some({ row: 2, col: 0, rows: 1, cols: 6 })

Positions are 0-based and reflect the final layout after all alignment, padding, and composition has been resolved.

Cursor navigation

Once you know where a reactive box is, use cursorToReactive to generate an ANSI cursor-movement command:

import * as Reactive from "effect-boxes/Reactive";
import { pipe, Option } from "effect";
 
const positions = Reactive.getPositions(layout);
const moveCmd = Reactive.cursorToReactive(positions, "footer");
 
pipe(
  moveCmd,
  Option.match({
    onNone: () => console.log("ID not found"),
    onSome: (cmd) => {
      // cmd is a Box<AnsiStyle> containing cursor movement escape codes
      console.log(Box.renderPrettySync(cmd));
    },
  })
);

Returns Option.none() if the ID doesn't exist in the position map, so it's safe to use without runtime errors.

Practical example: highlight active item

Combine reactive positions with Cmd to update a specific region without re-rendering the entire layout:

import * as Box from "effect-boxes/Box";
import * as Cmd from "effect-boxes/Cmd";
import * as Reactive from "effect-boxes/Reactive";
import * as Ansi from "effect-boxes/Ansi";
import { pipe, Option } from "effect";
 
// Build a menu with reactive items
const menu = Box.vcat(
  [
    Reactive.makeReactive(Box.text("  File  "), "item-0"),
    Reactive.makeReactive(Box.text("  Edit  "), "item-1"),
    Reactive.makeReactive(Box.text("  View  "), "item-2"),
  ],
  Box.left
);
 
// Get positions after layout
const positions = Reactive.getPositions(menu);
 
// Highlight item at index 1
const highlightItem = (index: number) => {
  const id = `item-${index}`;
  return pipe(
    Reactive.cursorToReactive(positions, id),
    Option.map((moveCmd) =>
      Box.combine(moveCmd, Box.text("> Active <").pipe(Box.annotate(Ansi.bold)))
    )
  );
};

When to use Reactive

  • Interactive prompts, to track menu items for keyboard navigation
  • Dashboards, to update individual panels without full re-render
  • Forms, to map cursor position to input fields

Reactive annotations compose with other annotations. A box can be both styled with Ansi and tracked with Reactive by nesting compositions.

API reference

See the full Reactive API reference for type signatures and additional utilities.