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:
{
"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
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 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
Feature JSX API Imperative API Syntax Declarative, React-like Procedural, manual State Reactive signals Manual updates Updates Automatic reconciliation Manual property setters Event Loop createLoop() helperCustom while loop Learning Curve Familiar to React devs Direct control Use Case Rapid UI development Fine-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
Use Signals for Dynamic State
Any value that changes over time should be a signal.
Static Props Don’t Need Signals
Fixed values can be passed directly:
< Text content = "Static label" width = { 20 } height = { 1 } />
Use ref callbacks to store widget references:
let inputRef : Widget | null = null ;
< Input
ref = { ( w ) => ( inputRef = w ) }
focusable = { true }
/>
// Later:
inputRef ?. focus ();
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