Skip to main content
The JSX API provides a declarative, React-like approach to building terminal UIs. It uses signals for reactive state and automatically reconciles the widget tree when state changes.

Setup

Configure your tsconfig.json for JSX support:
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "kraken-tui"
  }
}
Import the core JSX API:
import { Kraken, signal, render, createLoop } from "kraken-tui";
JSX files must use the .tsx extension. The reconciler is built on @preact/signals-core for reactive state management.

Signals: Reactive State

Signals are the foundation of reactivity in Kraken TUI. When a signal’s value changes, all UI elements bound to it automatically update.

Creating Signals

import { signal } from "kraken-tui";

const statusText = signal("Ready");
const count = signal(0);
const theme = signal("dark");

Reading Signal Values

// Access the current value
console.log(statusText.value); // "Ready"

// Update the value
statusText.value = "Processing...";
count.value += 1;

Using Signals in JSX

Pass signals directly as props. The reconciler automatically binds them:
const content = signal("Hello, World!");
const color = signal("#89b4fa");

const tree = (
  <Box width="100%" height="100%" padding={1}>
    <Text content={content} fg={color} width="100%" height={1} />
  </Box>
);
When you update the signal, the UI re-renders automatically:
// UI updates immediately
content.value = "Hello, Kraken!";
color.value = "#a6e3a1";

JSX Elements

JSX elements map directly to Kraken widgets:

Box (Container)

<Box
  width="100%"
  height="100%"
  flexDirection="column"
  padding={1}
  gap={2}
  bg="#1e1e2e"
  fg="#cdd6f4"
>
  {/* children */}
</Box>

Text (Display)

<Text
  content="# Heading\n\nBody text"
  format="markdown"
  fg="#89b4fa"
  bold={true}
  width="100%"
  height={3}
/>

Input (Single-line)

<Input
  width={30}
  height={3}
  border="rounded"
  fg="#cdd6f4"
  bg="#1e1e2e"
  maxLength={40}
  focusable={true}
  ref={(widget) => (inputRef = widget)}
/>

Select (Options)

<Select
  options={["Option 1", "Option 2", "Option 3"]}
  width={25}
  height={7}
  border="rounded"
  fg="#cdd6f4"
  focusable={true}
/>

ScrollBox (Scrollable)

<ScrollBox width="100%" height={12} border="single" fg="#6c7086">
  <Text content={longText} width="100%" height={40} />
</ScrollBox>

TextArea (Multi-line)

<TextArea
  width={60}
  height={15}
  border="rounded"
  wrap={true}
  fg="#cdd6f4"
  focusable={true}
/>

Component Patterns

Function Components

Create reusable components as functions:
function StatusBar({ message }: { message: string }) {
  return (
    <Box width="100%" height={1} bg="#1e1e2e">
      <Text content={message} fg="#585b70" width="100%" height={1} />
    </Box>
  );
}

// Use in your tree
const tree = (
  <Box width="100%" height="100%" flexDirection="column">
    <StatusBar message="Ready" />
  </Box>
);

Reactive Components

Combine signals with components:
function Counter() {
  const count = signal(0);
  const color = signal("#89b4fa");

  return (
    <Box flexDirection="column" gap={1}>
      <Text
        content={count.value.toString()}
        fg={color}
        bold={true}
        width={20}
        height={1}
      />
      <Text
        content="Press Space to increment"
        fg="#585b70"
        width={30}
        height={1}
      />
    </Box>
  );
}
Component functions receive props and return a JSX element. They’re called during mount and reconciliation.

Layout Components

function Card({ title, children }: { title: string; children: any }) {
  return (
    <Box
      flexDirection="column"
      border="rounded"
      padding={1}
      gap={1}
      width="100%"
    >
      <Text content={title} bold={true} fg="#89b4fa" width="100%" height={1} />
      {children}
    </Box>
  );
}

// Usage
<Card title="Settings">
  <Text content="Option 1" width="100%" height={1} />
  <Text content="Option 2" width="100%" height={1} />
</Card>

Rendering and Mounting

Initial Render

Mount the JSX tree and set it as the application root:
import { Kraken, render } from "kraken-tui";

const app = Kraken.init();

const tree = (
  <Box width="100%" height="100%" padding={1}>
    <Text content="Hello, Kraken!" width="100%" height={1} />
  </Box>
);

const rootInstance = render(tree, app);
render() mounts the entire widget tree, applies all props, binds signals, and sets the root. It returns an Instance representing the mounted tree.

Component Lifecycle

The reconciler manages mounting, updating, and unmounting:
  • Mount: Create native widgets, apply props, bind signals
  • Update: Reconcile props and children when signals change
  • Unmount: Dispose signal effects, destroy native widgets

Event Handling with createLoop

The createLoop helper provides an animation-aware event loop with automatic JSX event handler dispatch:
import { createLoop, KeyCode } from "kraken-tui";
import type { KrakenEvent } from "kraken-tui";

const loop = createLoop({
  app,
  onEvent(event: KrakenEvent) {
    if (event.type === "key" && event.keyCode === KeyCode.Escape) {
      loop.stop();
    }
  },
  onTick() {
    // Update state or metrics each frame
  },
});

await loop.start();
app.shutdown();

Event Handler Props

Attach event handlers directly to JSX elements:
let inputWidget = null;

const tree = (
  <Box width="100%" height="100%" padding={1}>
    <Input
      width={30}
      height={3}
      border="rounded"
      focusable={true}
      ref={(w) => (inputWidget = w)}
      onSubmit={(event) => {
        const value = inputWidget.getValue();
        statusText.value = `Submitted: ${value}`;
      }}
      onChange={(event) => {
        // Fired on every keystroke
      }}
    />
  </Box>
);
Supported handler props:
  • onKey — Keyboard events
  • onMouse — Mouse events
  • onFocus — Focus change
  • onChange — Value change (Input, Select, TextArea)
  • onSubmit — Enter key or selection confirmed
  • onAccessibility — Accessibility events (screen reader integration)

Complete JSX Example

Here’s a full interactive application using JSX and signals:
examples/migration-jsx.tsx
import {
  Kraken,
  signal,
  render,
  createLoop,
  KeyCode,
} from "kraken-tui";
import type { KrakenEvent, Widget } from "kraken-tui";

const statusText = signal("Press Tab to focus, Esc to quit");
const theme = signal("dark");
const bgColor = signal("#1e1e2e");
const fgColor = signal("#cdd6f4");

let inputWidget: Widget | null = null;
let selectWidget: Widget | null = null;

const themes = {
  dark: { bg: "#1e1e2e", fg: "#cdd6f4" },
  light: { bg: "#eff1f5", fg: "#4c4f69" },
};

function applyTheme(name: string) {
  const t = themes[name];
  if (t) {
    bgColor.value = t.bg;
    fgColor.value = t.fg;
    statusText.value = `Theme: ${name}`;
  }
}

const tree = (
  <Box
    width="100%"
    height="100%"
    flexDirection="column"
    padding={1}
    gap={1}
    bg={bgColor}
  >
    <Text
      content="# Kraken TUI (JSX)"
      format="markdown"
      fg="#89b4fa"
      width="100%"
      height={3}
    />

    <Box flexDirection="row" gap={2}>
      <Text content="Name:" bold={true} fg="#a6e3a1" width={6} height={3} />
      <Input
        width={30}
        height={3}
        border="rounded"
        fg={fgColor}
        bg={bgColor}
        maxLength={40}
        focusable={true}
        ref={(w) => (inputWidget = w)}
        onSubmit={(event) => {
          const value = inputWidget?.getValue() ?? "";
          statusText.value = `Submitted: "${value}"`;
        }}
      />

      <Text content="Theme:" bold={true} fg="#f9e2af" width={7} height={3} />
      <Select
        options={["dark", "light"]}
        width={20}
        height={5}
        border="rounded"
        fg={fgColor}
        focusable={true}
        ref={(w) => (selectWidget = w)}
        onChange={(event) => {
          if (event.selectedIndex != null) {
            const idx = event.selectedIndex;
            const opt = selectWidget?.getOption(idx);
            if (opt) applyTheme(opt);
          }
        }}
      />
    </Box>

    <Text content={statusText} fg="#585b70" width="100%" height={1} />
  </Box>
);

const app = Kraken.init();
const rootInstance = render(tree, app);

if (inputWidget) inputWidget.focus();

const loop = createLoop({
  app,
  onEvent(event: KrakenEvent) {
    if (event.type === "key" && event.keyCode === KeyCode.Escape) {
      loop.stop();
    }
  },
});

await loop.start();
app.shutdown();

Advanced Patterns

Computed Values

Derive state from other signals:
import { signal, computed } from "kraken-tui";

const count = signal(0);
const doubled = computed(() => count.value * 2);

const tree = (
  <Box flexDirection="column" gap={1}>
    <Text content={`Count: ${count.value}`} width={20} height={1} />
    <Text content={`Doubled: ${doubled.value}`} width={20} height={1} />
  </Box>
);

// Updating count automatically updates doubled
count.value += 1;

Effect Side-Effects

Run code when signals change:
import { signal, effect } from "kraken-tui";

const message = signal("");

effect(() => {
  console.log("Message changed:", message.value);
});

message.value = "Hello!"; // Logs: "Message changed: Hello!"

Batched Updates

Batch multiple signal updates to prevent intermediate renders:
import { signal, batch } from "kraken-tui";

const count = signal(0);
const status = signal("idle");

batch(() => {
  count.value = 100;
  status.value = "updated";
});
// UI renders once with both updates

Keyed Children

Optimize list reconciliation with keys:
const items = signal(["Item 1", "Item 2", "Item 3"]);

const tree = (
  <Box flexDirection="column" gap={1}>
    {items.value.map((item, index) => (
      <Text key={item} content={item} width="100%" height={1} />
    ))}
  </Box>
);

// Adding/removing items reconciles efficiently
items.value = [...items.value, "Item 4"];

JSX vs Imperative API

FeatureJSX APIImperative API
SyntaxDeclarative, React-likeProcedural, manual
StateReactive signalsManual updates
UpdatesAutomatic reconciliationManual property setters
Event LoopcreateLoop() helperCustom while loop
Learning CurveFamiliar to React devsDirect control
Use CaseRapid UI developmentFine-grained control
You can mix both APIs! Use JSX for the main UI and drop down to imperative methods for specific widgets via ref callbacks.

Best Practices

1
Use Signals for Dynamic State
2
Any value that changes over time should be a signal.
3
Static Props Don’t Need Signals
4
Fixed values can be passed directly:
5
<Text content="Static label" width={20} height={1} />
6
Store Widget Refs for Imperative Access
7
Use ref callbacks to store widget references:
8
let inputRef: Widget | null = null;

<Input
  ref={(w) => (inputRef = w)}
  focusable={true}
/>

// Later:
inputRef?.focus();
9
Clean Up Effects
10
Effects created outside JSX must be disposed manually. The reconciler handles JSX-bound signals automatically.

Next Steps

Event Handling

Handle keyboard, mouse, and focus events

Advanced Patterns

Animation choreography and performance optimization

Widgets

Complete widget API reference

Signals

Deep dive into reactive state

Build docs developers (and LLMs) love