Skip to content

re-marked/yokai

Repository files navigation

yokai

Serious React TUI renderer for serious CLI apps. Pure TypeScript Yoga layout, diff-based rendering, ScrollBox, alt buffer, mouse events, draggable and resizable components.

drag and drop in the terminal

Used by claude-corp. Forked from claude-code-kit, itself a fork of Ink.

Packages

Package Description
@yokai-tui/renderer React reconciler, component library, event system, terminal I/O
@yokai-tui/shared Pure-TypeScript Yoga layout engine, logging, env helpers

How it works

React component tree
  → Reconciler (React 19 host config)
  → DOM + Yoga layout
  → Tree traversal + text wrapping
  → Screen buffer (cell grid)
  → Frame diff → ANSI patches
  → stdout

The renderer double-buffers frames, diffs cell-by-cell, and emits only the minimal ANSI patch sequence each tick. A spinner update or a line of streamed text touches O(changed cells), not O(rows × cols).

Components

import { render, Box, Text, ScrollBox } from '@yokai-tui/renderer'

function App() {
  return (
    <Box flexDirection="column" height="100%">
      <Box flexGrow={1}>
        <ScrollBox stickyScroll>
          <Text>streaming content…</Text>
        </ScrollBox>
      </Box>
      <Box>
        <Text>footer</Text>
      </Box>
    </Box>
  )
}

render(<App />)

<Box> — flex container. Accepts all Yoga layout props: flexDirection, flexGrow, flexShrink, flexBasis, alignItems, justifyContent, gap, margin, padding, width, height, position, top, left, right, bottom, overflow, zIndex (only honored on position: 'absolute').

<Text> — text node. Accepts color, backgroundColor, bold, italic, underline, dimColor, wrap.

<ScrollBox> — scrollable container with imperative scroll API, sticky scroll, viewport culling, and DECSTBM hardware scroll hints.

<AlternateScreen> — enters the terminal alternate buffer with optional mouse tracking on mount, exits cleanly on unmount.

<Link> — OSC 8 hyperlink.

<Draggable> — gesture-captured drag primitive. Raise-on-press, drag-time z boost, optional bounds clamp, onDragStart / onDrag / onDragEnd. dragData payload forwarded to drop targets.

<DropTarget> — receiver side of drag-and-drop. Optional accept(data) => boolean filter, hover lifecycle (onDragEnter / onDragOver / onDragLeave), onDrop on the topmost target containing the cursor at release.

<Resizable> — resize primitive with s, e, se handles. Hover-highlighted chrome, minSize / maxSize clamping, onResizeStart / onResize / onResizeEnd. Defaults overflow: 'hidden' to keep content from bleeding outside the box.

<FocusGroup> — adds arrow-key navigation between focusable descendants without interfering with Tab. direction="row" | "column" | "both", optional wrap, optional isActive. Tab still cycles globally; arrows are bounded to the group.

<FocusRing> — focusable Box with built-in focus-visible border indicator (default cyan border on focus). Pair with <FocusGroup> for keyboard-navigable lists / menus.

<Checkbox>[x] / [ ] boolean toggle. Click / Enter / Space toggles. Defaults claimFocusOnClick={false} so toggling doesn't tear focus from a peer (e.g. live-bound <TextInput>).

<Radio>(x) / ( ) mutually-exclusive selection bound to a value. Same focus model as <Checkbox>. Compose with <FocusGroup> for arrow-key navigation.

<TextInput> — editable text. Single-line or multiline. Caret via the real terminal cursor (IME / a11y correct). Smart bracketed paste, undo/redo, word nav, selection, password masking. Focus-color border by default (configurable via borderColorFocus). Configurable cursor shape / blink / color via cursorStyle / cursorBlink / cursorColor. See docs/components/text-input.md.

ScrollBox API

const ref = useRef<ScrollBoxRef>(null)

<ScrollBox ref={ref} stickyScroll>
  {items}
</ScrollBox>

// Imperative scroll
ref.current.scrollTo(100)
ref.current.scrollBy(10)
ref.current.scrollToBottom()
ref.current.scrollToElement(elementRef, { offset: 2 })

Mouse and keyboard

Mouse tracking is enabled inside <AlternateScreen mouseTracking>. Events are dispatched through the component tree with capture and bubble phases, matching browser event semantics.

<Box
  onClick={(e) => console.log(e.col, e.row)}
  onMouseDown={(e) => console.log(e.col, e.row)}
  onMouseEnter={() => console.log('hover')}
  onMouseLeave={() => console.log('leave')}
  onKeyDown={(e) => console.log(e.key)}
>

Gesture capture

Inside onMouseDown, call event.captureGesture({ onMove, onUp }) to claim subsequent mouse-motion events and the eventual release for that one drag. Selection extension is suppressed for the duration; the captured handlers fire even when the cursor leaves the originally-pressed element's bounds.

<Box
  onMouseDown={(e) => {
    e.captureGesture({
      onMove: (m) => console.log('drag at', m.col, m.row),
      onUp: (u) => console.log('release at', u.col, u.row),
    })
  }}
/>

<Draggable>, <DropTarget>, and <Resizable> are all built on top of this primitive. Reach for them first; reach for raw captureGesture when none of the components fit your interaction.

Keyboard navigation

Tab / Shift+Tab cycle through every tabIndex >= 0 element in the tree (built into the renderer — no setup needed). Arrow-key navigation is opt-in via <FocusGroup>, scoped to its descendants — Tab still cycles globally, arrows are bounded.

import { FocusGroup, FocusRing, Text, useFocusManager } from '@yokai-tui/renderer'

function Menu({ items }) {
  return (
    <FocusGroup direction="column" wrap>
      {items.map((item) => (
        <FocusRing key={item.id} paddingX={1}>
          <Text>{item.label}</Text>
        </FocusRing>
      ))}
    </FocusGroup>
  )
}

<FocusRing> adds a focus-visible border (default cyan) to the focused item — a thin Box wrapper around useFocus that you can replace with custom chrome by inlining useFocus() directly.

For programmatic focus (modal pulling focus on open, status bar showing the focused element), use useFocusManager():

const { focused, focus, focusNext, focusPrevious, blur } = useFocusManager()

For per-element tracking, useFocus():

const { ref, isFocused, focus } = useFocus({ autoFocus: true })
return <Box ref={ref} tabIndex={0} ...>...</Box>

See pnpm demo:focus-nav for a live example with two <FocusGroup>s side-by-side, one column-direction and one row-direction.

Demos

pnpm demo:drag                # three overlapping draggable rectangles
pnpm demo:constrained-drag    # constrained-vs-free drag inside containers
pnpm demo:drag-and-drop       # kanban: cards into columns
pnpm demo:resizable           # two panels, three handles each
pnpm demo:focus-nav           # Tab + arrow navigation between FocusGroups
pnpm demo:text-input          # text input fields: single, multiline, password

Each demo lives in examples/ and is a small .tsx file you can read top-to-bottom — they're meant to be the first place you look when wiring a new interaction.

Install

pnpm add @yokai-tui/renderer react
# or
npm install @yokai-tui/renderer react

See release notes for what's in each version.

Hooks

Hook Description
useInput(handler, options?) Raw keyboard input
useApp() { exit }
useStdin() Stdin stream + isRawModeSupported
useStdout() Stdout stream + write
useTerminalViewport() [ref, entry], where entry.isVisible tracks whether the referenced element is currently in the terminal viewport
useFocus(options?) { ref, isFocused, focus } — per-element focus tracking + imperative focus
useFocusManager() { focused, focus, focusNext, focusPrevious, blur } — global focus actions, reactive to changes
useInterval(fn, ms) Stable interval that cleans up on unmount

Development

pnpm install
pnpm build          # shared → renderer
pnpm typecheck      # both packages
pnpm lint           # biome
pnpm test           # vitest

Documentation

Full reference under docs/ — getting started, conceptual guides, per-component and per-hook reference, real-world patterns, migration from Ink, and an AGENTS.md for AI assistants writing code that uses yokai.

Contributing

See CONTRIBUTING.md for the rules of the road — branching, granular commits, co-authorship, quality bar, and release workflow.

License

MIT

About

Serious React TUI renderer for serious CLI apps. Pure TypeScript Yoga layout, diff-based rendering, ScrollBox, alt buffer, mouse events, draggable and resizable components.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors