Once you’re comfortable with the basics, these advanced patterns will help you build polished, performant terminal UIs.
Animation Choreography
Choreography allows you to coordinate multiple animations on a single timeline.
Creating a Choreography Group
import { Kraken } from "kraken-tui" ;
const app = Kraken . init ();
// Create a choreography group
const choreo = app . createChoreoGroup ();
// Create individual animations
const fadeIn = box1 . animate ( "opacity" , 1.0 , 500 , "easeOut" );
const slideIn = box2 . animate ( "positionY" , 10 , 500 , "easeOut" );
const colorChange = text . animate ( "fgColor" , "#a6e3a1" , 500 , "linear" );
// Add to timeline with offsets (in milliseconds)
app . choreoAdd ( choreo , fadeIn , 0 ); // Start immediately
app . choreoAdd ( choreo , slideIn , 100 ); // Start 100ms later
app . choreoAdd ( choreo , colorChange , 200 ); // Start 200ms later
// Start the entire sequence
app . startChoreo ( choreo );
Staggered Entrance Animations
function staggerIn ( widgets : Widget [], delayMs : number ) {
const choreo = app . createChoreoGroup ();
widgets . forEach (( widget , index ) => {
// Start each widget invisible
widget . setOpacity ( 0 );
// Create fade-in animation
const anim = widget . animate ( "opacity" , 1.0 , 300 , "easeOut" );
// Add to timeline with staggered delay
app . choreoAdd ( choreo , anim , index * delayMs );
});
app . startChoreo ( choreo );
return choreo ;
}
// Usage: fade in 5 cards with 100ms stagger
const cards = [ card1 , card2 , card3 , card4 , card5 ];
const entrance = staggerIn ( cards , 100 );
Sequential Animation Chains
// Chain animations to run one after another
const fadeOut = box . animate ( "opacity" , 0.0 , 300 , "easeIn" );
const slideDown = box . animate ( "positionY" , 50 , 300 , "easeIn" );
// slideDown starts when fadeOut completes
app . chainAnimation ( fadeOut , slideDown );
box . startAnimation ( fadeOut );
Coordinated UI Transitions
examples/accessibility-demo.tsx
import { signal } from "kraken-tui" ;
// Animate multiple properties in sync
function transitionTheme (
widgets : Widget [],
newBg : string ,
newFg : string ,
durationMs : number
) {
const choreo = app . createChoreoGroup ();
widgets . forEach (( widget ) => {
const bgAnim = widget . animate ( "bgColor" , newBg , durationMs , "easeInOut" );
const fgAnim = widget . animate ( "fgColor" , newFg , durationMs , "easeInOut" );
app . choreoAdd ( choreo , bgAnim , 0 );
app . choreoAdd ( choreo , fgAnim , 0 );
});
app . startChoreo ( choreo );
}
// Smooth theme transition across all widgets
transitionTheme (
[ root , header , input , select ],
"#eff1f5" , // Light mode background
"#4c4f69" , // Light mode foreground
500
);
Cancelling Choreography
const choreo = app . createChoreoGroup ();
// ... add animations ...
app . startChoreo ( choreo );
// Cancel the entire choreography
app . cancelChoreo ( choreo );
// Clean up when done
app . destroyChoreoGroup ( choreo );
Choreography groups are lightweight. Create them on-demand and destroy after use to avoid resource leaks.
Custom Themes
Themes provide a centralized way to manage visual styles.
Creating a Custom Theme
import { Theme } from "kraken-tui" ;
// Create a new theme
const myTheme = new Theme ();
// Set per-node-type defaults
myTheme . setTypeColor ( "text" , "fg" , "#cdd6f4" );
myTheme . setTypeColor ( "text" , "bg" , "#1e1e2e" );
myTheme . setTypeColor ( "box" , "bg" , "#181825" );
myTheme . setTypeColor ( "input" , "fg" , "#f5e0dc" );
myTheme . setTypeColor ( "input" , "bg" , "#313244" );
// Set borders
myTheme . setTypeBorderStyle ( "input" , "rounded" );
myTheme . setTypeBorderStyle ( "select" , "rounded" );
myTheme . setTypeBorderStyle ( "text" , "none" );
// Set flags (bold, italic, underline)
myTheme . setTypeFlag ( "text" , "bold" , false );
// Set opacity
myTheme . setTypeOpacity ( "box" , 1.0 );
// Apply to current root
app . switchTheme ( myTheme );
// Or bind to a specific subtree
myTheme . applyTo ( container . handle );
Built-in Themes
import { Theme } from "kraken-tui" ;
// Built-in dark theme (handle 1)
app . switchTheme ( Theme . DARK );
// Built-in light theme (handle 2)
app . switchTheme ( Theme . LIGHT );
Theme Inheritance
Themes cascade down the widget tree. Child widgets inherit parent theme properties unless explicitly overridden:
// Apply base theme to root
baseTheme . applyTo ( root . handle );
// Override specific subtree with accent theme
accentTheme . applyTo ( sidebar . handle );
Dynamic Theme Switching
const themes = {
dark: createDarkTheme (),
light: createLightTheme (),
solarized: createSolarizedTheme (),
};
let currentTheme = "dark" ;
function switchTheme ( name : string ) {
const theme = themes [ name ];
if ( theme ) {
app . switchTheme ( theme );
currentTheme = name ;
}
}
// User selects theme
for ( const event of app . drainEvents ()) {
if ( event . type === "submit" && event . target === themeSelect . handle ) {
const idx = themeSelect . getSelected ();
const themeName = themeSelect . getOption ( idx );
switchTheme ( themeName );
}
}
Normalizing Theme Defaults
// Explicitly normalize borders for a clean demo
function createDemoTheme () {
const theme = new Theme ();
// Set base colors
theme . setTypeColor ( "box" , "bg" , "#1e1e2e" );
theme . setTypeColor ( "text" , "fg" , "#cdd6f4" );
// Normalize borders: none by default
theme . setTypeBorderStyle ( "box" , "none" );
theme . setTypeBorderStyle ( "text" , "none" );
// Explicit borders only where needed
theme . setTypeBorderStyle ( "input" , "rounded" );
theme . setTypeBorderStyle ( "select" , "rounded" );
return theme ;
}
Theme property setters use masks to track which properties have been set. Unset properties fall through to Rust defaults.
Theme Cleanup
// Destroy custom themes when done
myTheme . destroy ();
// Built-in themes (DARK, LIGHT) are global and must NOT be destroyed
Animation-Aware Event Loop
Optimize CPU usage by adapting the event loop to animation state:
import { PERF_ACTIVE_ANIMATIONS } from "kraken-tui/loop" ;
while ( running ) {
const animating = app . getPerfCounter ( PERF_ACTIVE_ANIMATIONS ) > 0 n ;
if ( animating ) {
// Non-blocking input, render at 60fps
app . readInput ( 0 );
await Bun . sleep ( 16 );
} else {
// Block on input when idle (saves CPU)
app . readInput ( 100 ); // Wait up to 100ms
}
for ( const event of app . drainEvents ()) {
// Handle events
}
app . render ();
}
Reducing Reflows
Minimize layout recalculations:
// BAD: Multiple setters trigger multiple layouts
box . setWidth ( 100 );
box . setHeight ( 50 );
box . setPadding ( 2 );
box . setGap ( 1 );
// GOOD: Batch updates in constructor
const box = new Box ({
width: 100 ,
height: 50 ,
padding: 2 ,
gap: 1 ,
});
Lazy Rendering
Only update UI when state changes:
import { signal , batch } from "kraken-tui" ;
const data = signal ([]);
const dirty = signal ( false );
function updateData ( newData : any []) {
batch (() => {
data . value = newData ;
dirty . value = true ;
});
}
const loop = createLoop ({
app ,
onTick () {
if ( dirty . value ) {
// UI automatically re-renders via signal binding
dirty . value = false ;
}
},
});
Reuse widgets instead of creating/destroying:
class WidgetPool {
private pool : Widget [] = [];
acquire () : Widget {
return this . pool . pop () ?? new Text ({ width: 20 , height: 1 });
}
release ( widget : Widget ) : void {
widget . setVisible ( false );
this . pool . push ( widget );
}
clear () : void {
for ( const widget of this . pool ) {
widget . destroySubtree ();
}
this . pool . length = 0 ;
}
}
const textPool = new WidgetPool ();
// Acquire from pool
const text = textPool . acquire ();
text . setContent ( "New content" );
text . setVisible ( true );
// Release back to pool
textPool . release ( text );
// Query performance counters
const nodeCount = app . getNodeCount ();
const activeAnims = app . getPerfCounter ( PERF_ACTIVE_ANIMATIONS );
console . log ( `Nodes: ${ nodeCount } , Animations: ${ activeAnims } ` );
// Enable debug overlay
app . setDebug ( true );
Use app.setDebug(true) during development to see render stats overlaid in the terminal.
Testing Patterns
Headless Testing
Test without a real terminal:
import { Kraken } from "kraken-tui" ;
// Initialize in headless mode
const app = Kraken . initHeadless ( 80 , 24 );
// Build UI
const root = new Box ({ width: "100%" , height: "100%" });
const text = new Text ({ content: "Test" , width: 10 , height: 1 });
root . append ( text );
app . setRoot ( root );
// Simulate input (not yet implemented in API)
// Test event handling logic
const events = app . drainEvents ();
// Verify state
const value = text . getContent ();
if ( value !== "Test" ) {
throw new Error ( "Content mismatch" );
}
app . shutdown ();
import { test , expect } from "bun:test" ;
import { Kraken , Box , Text } from "kraken-tui" ;
test ( "Text widget sets content" , () => {
const app = Kraken . initHeadless ( 80 , 24 );
const text = new Text ({ content: "Initial" , width: 20 , height: 1 });
text . setContent ( "Updated" );
expect ( text . getContent ()). toBe ( "Updated" );
app . shutdown ();
});
test ( "Box appends children" , () => {
const app = Kraken . initHeadless ( 80 , 24 );
const box = new Box ({ width: 100 , height: 100 });
const child1 = new Text ({ content: "A" , width: 10 , height: 1 });
const child2 = new Text ({ content: "B" , width: 10 , height: 1 });
box . append ( child1 );
box . append ( child2 );
expect ( box . getChildCount ()). toBe ( 2 );
app . shutdown ();
});
Snapshot Testing
import { test , expect } from "bun:test" ;
import { Kraken , render } from "kraken-tui" ;
test ( "renders dashboard correctly" , () => {
const app = Kraken . initHeadless ( 80 , 24 );
const tree = (
< Box width = "100%" height = "100%" padding = { 1 } >
< Text content = "Dashboard" bold = { true } width = "100%" height = { 1 } />
</ Box >
);
const instance = render ( tree , app );
// Render to capture output
app . render ();
// In a real test, compare against snapshot
// expect(output).toMatchSnapshot();
app . shutdown ();
});
Event Simulation
function simulateKeyPress ( app : Kraken , keyCode : number ) {
// Not yet directly supported; test event handlers manually
const event : KrakenEvent = {
type: "key" ,
target: 0 ,
keyCode ,
char: "" ,
ctrl: false ,
alt: false ,
shift: false ,
};
// Call your event handler directly
handleEvent ( event );
}
test ( "Escape key stops loop" , () => {
let stopped = false ;
function handleEvent ( event : KrakenEvent ) {
if ( event . type === "key" && event . keyCode === KeyCode . Escape ) {
stopped = true ;
}
}
simulateKeyPress ( app , KeyCode . Escape );
expect ( stopped ). toBe ( true );
});
Runtime Subtree Operations
Dynamically insert and remove widget subtrees:
Inserting Subtrees
// Create a detached subtree
const banner = new Box ({
width: "100%" ,
height: 5 ,
border: "rounded" ,
padding: 1 ,
});
banner . append ( new Text ({ content: "Alert!" , bold: true , width: "100%" , height: 1 }));
// Insert at specific index
root . insertChild ( banner , 0 ); // Insert at top
Removing Subtrees
// Destroy entire subtree in one FFI call
banner . destroySubtree ();
Conditional Rendering
let errorBanner : Box | null = null ;
function showError ( message : string ) {
if ( ! errorBanner ) {
errorBanner = new Box ({
width: "100%" ,
height: 3 ,
border: "single" ,
padding: 1 ,
});
const text = new Text ({ content: message , fg: "#f38ba8" , width: "100%" , height: 1 });
errorBanner . append ( text );
root . insertChild ( errorBanner , 0 );
}
}
function hideError () {
if ( errorBanner ) {
errorBanner . destroySubtree ();
errorBanner = null ;
}
}
Accessibility Best Practices
Make your TUI accessible to screen readers:
import { AccessibilityRole } from "kraken-tui" ;
// Set role
input . setRole ( AccessibilityRole . Input );
submitButton . setRole ( AccessibilityRole . Button );
header . setRole ( AccessibilityRole . Heading );
// Set labels and descriptions
input . setLabel ( "Full name" );
input . setDescription ( "Enter your full name" );
submitButton . setLabel ( "Submit form" );
submitButton . setDescription ( "Press Enter to submit" );
JSX Accessibility Props
examples/accessibility-demo.tsx
< Input
width = { 30 }
height = { 3 }
border = "rounded"
role = "input"
aria-label = "Full name"
aria-description = "Enter your full name"
focusable = { true }
/>
< Text
content = "[Submit]"
border = "rounded"
width = { 20 }
height = { 3 }
role = "button"
aria-label = "Submit form"
aria-description = "Press Enter to submit"
focusable = { true }
/>
Handle Accessibility Events
const a11yLog = signal ( "(no events)" );
const loop = createLoop ({
app ,
onEvent ( event ) {
if ( event . type === "accessibility" ) {
const roleCode = event . roleCode ?? 0 ;
const roleName = getRoleName ( roleCode );
a11yLog . value = `Focus -> role= ${ roleName } ` ;
}
},
});
Common Patterns
let debounceTimer : Timer | null = null ;
for ( const event of app . drainEvents ()) {
if ( event . type === "change" && event . target === searchInput . handle ) {
if ( debounceTimer ) clearTimeout ( debounceTimer );
debounceTimer = setTimeout (() => {
const query = searchInput . getValue ();
performSearch ( query );
}, 300 );
}
}
Loading Indicators
const spinner = [ "|" , "/" , "-" , " \\ " ];
let spinnerFrame = 0 ;
const loop = createLoop ({
app ,
onTick () {
if ( isLoading ) {
spinnerFrame = ( spinnerFrame + 1 ) % spinner . length ;
statusText . setContent ( `Loading ${ spinner [ spinnerFrame ] } ` );
}
},
});
Modal Dialogs
function showModal ( title : string , message : string ) : Promise < boolean > {
const modal = new Box ({
width: 50 ,
height: 10 ,
border: "rounded" ,
padding: 1 ,
});
const titleText = new Text ({ content: title , bold: true , width: "100%" , height: 1 });
const messageText = new Text ({ content: message , width: "100%" , height: 5 });
const buttons = new Box ({ flexDirection: "row" , gap: 2 });
const okButton = new Text ({ content: "[OK]" , width: 10 , height: 1 , border: "single" });
const cancelButton = new Text ({ content: "[Cancel]" , width: 10 , height: 1 , border: "single" });
buttons . append ( okButton );
buttons . append ( cancelButton );
modal . append ( titleText );
modal . append ( messageText );
modal . append ( buttons );
root . insertChild ( modal , 0 );
return new Promise (( resolve ) => {
// Wait for selection...
// Clean up and resolve
});
}
Best Practices Summary
Use Choreography for Complex Animations
Coordinate multiple animations on a timeline for polished transitions.
Set up themes before building your widget tree to ensure consistent styling.
Adapt rendering frequency based on animation state to save CPU.
Reuse widgets instead of creating/destroying for better performance.
Write unit tests without a real terminal using Kraken.initHeadless().
Annotate for Accessibility
Set roles, labels, and descriptions for screen reader support.
Next Steps
Animations API Complete animation and choreography reference
Themes API Theme creation and customization guide
Animation Concepts Learn about animation system design
Kraken API Performance monitoring and debugging