Skip to main content
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

examples/demo.ts
// 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

Performance Optimization

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) > 0n;
  
  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;
    }
  },
});

Widget Pooling

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);

Performance Monitoring

// 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();

Unit Testing Widgets

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:

Annotate Widgets

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

Debouncing Input

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]}`);
    }
  },
});
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

1
Use Choreography for Complex Animations
2
Coordinate multiple animations on a timeline for polished transitions.
3
Apply Themes Early
4
Set up themes before building your widget tree to ensure consistent styling.
5
Optimize Event Loop
6
Adapt rendering frequency based on animation state to save CPU.
7
Pool Widgets When Possible
8
Reuse widgets instead of creating/destroying for better performance.
9
Test in Headless Mode
10
Write unit tests without a real terminal using Kraken.initHeadless().
11
Annotate for Accessibility
12
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

Build docs developers (and LLMs) love