askill
opentui-dev

opentui-devSafety 95Repository

Build and modify terminal user interfaces using OpenTUI with React or Core API. Use when implementing terminal UIs, TUIs, CLI applications, interactive terminal components, keyboard navigation, terminal styling, or working on OpenTUI-based applications.

34 stars
1.2k downloads
Updated 2/8/2026

Package Files

Loading files...
SKILL.md

OpenTUI Development Skill

This skill provides comprehensive guidance for building terminal user interfaces (TUIs) using OpenTUI, a TypeScript library for creating component-based terminal applications with React or Core API.

When to use this skill

Use this skill when:

  • Building a new terminal user interface (TUI) or CLI application
  • Working with OpenTUI-based projects
  • Creating interactive terminal components (inputs, selects, lists, etc.)
  • Implementing keyboard navigation and event handling in terminals
  • Adding syntax highlighting or code display in terminal UIs
  • Creating diff viewers, text editors, or terminal-based forms
  • Styling terminal applications with borders, colors, and flexbox layouts
  • Debugging or enhancing OpenTUI applications
  • User mentions: TUI, terminal UI, CLI app, OpenTUI, terminal components

What is OpenTUI

OpenTUI is a TypeScript framework for building terminal user interfaces with:

  • Component-based architecture: Similar to web frameworks, compose UIs from reusable components
  • React integration: Use familiar React patterns with JSX for declarative terminal UIs
  • Flexbox layouts: CSS Flexbox-like layout system powered by Yoga layout engine
  • Rich components: Built-in inputs, selects, code viewers, diff viewers, and more
  • Type-safe: Full TypeScript support with strict mode
  • High performance: Native Zig rendering layer for optimal performance

Core Packages

  • @opentui/core - Core library with imperative API and primitives (standalone)
  • @opentui/react - React reconciler for declarative UI development
  • @opentui/solid - SolidJS reconciler (alternative framework)

Quick Start

Creating a new OpenTUI React project

bun create tui --template react
cd my-tui-app
bun install
bun run index.tsx

Manual installation

bun install @opentui/core @opentui/react react

Basic "Hello World" example

import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

function App() {
  return (
    <box style={{ padding: 2 }}>
      <text fg="#00FF00">Hello, OpenTUI!</text>
    </box>
  )
}

const renderer = await createCliRenderer({ useAlternateScreen: true })
createRoot(renderer).render(<App />)
renderer.start()

TypeScript Configuration

OpenTUI requires specific TypeScript settings:

{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "jsxImportSource": "@opentui/react",
    "strict": true,
    "skipLibCheck": true
  }
}

Key settings:

  • jsxImportSource: "@opentui/react" - Use OpenTUI's JSX factory
  • moduleResolution: "bundler" - Required for Bun
  • strict: true - Enable strict type checking

Core Concepts

1. Renderer

The renderer manages terminal output, input events, and the rendering loop.

import { createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer({
  exitOnCtrlC: true,           // Exit on Ctrl+C (default: true)
  targetFps: 30,               // Target frame rate
  maxFps: 60,                  // Maximum frame rate
  useAlternateScreen: true,    // Use alternate screen (full-screen)
  useMouse: true,              // Enable mouse support
  backgroundColor: "#000000",   // Background color
  useConsole: true,            // Enable console overlay
})

// Start the render loop
renderer.start()

Rendering Modes:

  • Live Mode: Call renderer.start() for continuous rendering at target FPS
  • On-Demand: Without start(), renders only when changes occur

2. Layout System (Flexbox)

OpenTUI uses Yoga for CSS Flexbox-like layouts:

<box
  flexDirection="row"          // row | column | row-reverse | column-reverse
  justifyContent="center"      // flex-start | flex-end | center | space-between | space-around
  alignItems="center"          // flex-start | flex-end | center | stretch | baseline
  gap={2}                      // Gap between children
  flexGrow={1}                 // Grow factor
  flexShrink={1}               // Shrink factor
  flexBasis="auto"             // Base size
  flexWrap="wrap"              // no-wrap | wrap | wrap-reverse
>
  {/* Children */}
</box>

Size Properties:

width={40}           // Fixed width in characters
height={10}          // Fixed height in lines
width="50%"          // Percentage of parent
width="auto"         // Auto-size based on content
minWidth={20}        // Minimum width
maxWidth={80}        // Maximum width

Spacing:

padding={2}          // All sides
paddingLeft={1}      // Individual sides
margin={1}           // Margin (all sides)
marginTop={2}        // Individual margins

Position:

position="relative"  // relative | absolute
top={5}             // Position from top
left={10}           // Position from left
zIndex={999}        // Stack order

3. Colors

Colors in OpenTUI use RGBA format or hex strings:

import { RGBA } from "@opentui/core"

// Hex strings (most common)
<text fg="#FFFFFF" bg="#000000">Text</text>

// RGBA objects
const red = RGBA.fromHex("#FF0000")
const blue = RGBA.fromInts(0, 0, 255, 255)  // RGB 0-255
const transparent = RGBA.fromValues(1.0, 1.0, 1.0, 0.5)  // RGBA 0.0-1.0

4. Text Attributes

Style text with bold, italic, underline, etc.:

import { TextAttributes } from "@opentui/core"

<text attributes={TextAttributes.BOLD}>Bold text</text>
<text attributes={TextAttributes.BOLD | TextAttributes.ITALIC}>Bold italic</text>

// Available attributes:
// BOLD, DIM, ITALIC, UNDERLINE, BLINK, INVERSE, HIDDEN, STRIKETHROUGH

React Components

Box Component

Container with borders, backgrounds, and layout:

<box
  title="Panel Title"
  border                       // Show border
  borderStyle="single"         // single | double | rounded | bold | classic
  borderColor="#FFFFFF"
  backgroundColor="#1a1a1a"
  focused={true}               // Highlight when focused
  padding={2}
  flexDirection="column"
  width={40}
  height={10}
>
  <text>Content</text>
</box>

Border Styles:

  • single - Single line (─│┌┐└┘)
  • double - Double line (═║╔╗╚╝)
  • rounded - Rounded corners (─│╭╮╰╯)
  • bold - Bold/thick line
  • classic - ASCII style (+-|)

Text Component

Display text with styling:

// Simple text
<text fg="#FFFFFF" bg="#000000">Simple text</text>

// Rich text with children
<text>
  <span fg="#FF0000">Red text</span>
  {" "}
  <strong>Bold</strong>
  {" "}
  <em>Italic</em>
  {" "}
  <u>Underlined</u>
  <br />
  <a href="https://example.com">Link</a>
</text>

// Formatted content
<text content="Preformatted content" fg="#00FF00" />

Input Component

Single-line text input:

const [value, setValue] = useState("")

<input
  placeholder="Type here..."
  value={value}
  focused={true}               // Must be focused to receive input
  onInput={setValue}           // Called on every keystroke
  onSubmit={(val) => {         // Called on Enter key
    console.log("Submitted:", val)
  }}
  style={{
    backgroundColor: "#1a1a1a",
    focusedBackgroundColor: "#2a2a2a",
  }}
/>

Textarea Component

Multi-line text editor:

import { useRef } from "react"
import type { TextareaRenderable } from "@opentui/core"

const textareaRef = useRef<TextareaRenderable>(null)

<textarea
  ref={textareaRef}
  placeholder="Type here..."
  focused={true}
  initialValue="Initial text"
  style={{ width: 60, height: 10 }}
/>

// Access value: textareaRef.current?.getText()

Select Component

List selection with scrolling:

import type { SelectOption } from "@opentui/core"

const options: SelectOption[] = [
  { name: "Option 1", description: "First option", value: "opt1" },
  { name: "Option 2", description: "Second option", value: "opt2" },
  { name: "Option 3", description: "Third option", value: "opt3" },
]

<select
  options={options}
  focused={true}
  onChange={(index, option) => {
    console.log(`Selected ${index}:`, option)
  }}
  showScrollIndicator
  style={{ height: 10 }}
/>

Navigation:

  • Up/Down arrows - Move selection
  • Enter - Confirm selection
  • Auto-scrolls to keep selection visible

Tab Select Component

Horizontal tab selection:

<tab-select
  options={[
    { name: "Home", description: "Dashboard view" },
    { name: "Settings", description: "Configuration" },
    { name: "Help", description: "Documentation" },
  ]}
  focused={true}
  onChange={(index, option) => {
    console.log("Tab changed:", option.name)
  }}
  tabWidth={20}
/>

Scrollbox Component

Scrollable container for long content:

<scrollbox
  focused={true}
  style={{
    height: 20,
    rootOptions: { backgroundColor: "#24283b" },
    wrapperOptions: { backgroundColor: "#1f2335" },
    viewportOptions: { backgroundColor: "#1a1b26" },
    contentOptions: { backgroundColor: "#16161e" },
    scrollbarOptions: {
      showArrows: true,
      trackOptions: {
        foregroundColor: "#7aa2f7",
        backgroundColor: "#414868",
      },
    },
  }}
>
  {/* Long scrollable content */}
  <text>Line 1</text>
  <text>Line 2</text>
  {/* ... many more lines */}
</scrollbox>

Code Component

Display code with syntax highlighting:

import { SyntaxStyle, RGBA } from "@opentui/core"

const syntaxStyle = SyntaxStyle.fromStyles({
  keyword: { fg: RGBA.fromHex("#ff6b6b"), bold: true },
  string: { fg: RGBA.fromHex("#51cf66") },
  comment: { fg: RGBA.fromHex("#868e96"), italic: true },
  function: { fg: RGBA.fromHex("#4dabf7") },
  number: { fg: RGBA.fromHex("#ffd43b") },
})

<code
  content={codeString}
  filetype="typescript"        // javascript, python, rust, go, etc.
  syntaxStyle={syntaxStyle}
/>

Line Number Component

Code with line numbers and diff highlights:

import { useRef, useEffect } from "react"
import type { LineNumberRenderable } from "@opentui/core"

const lineNumberRef = useRef<LineNumberRenderable>(null)

useEffect(() => {
  // Highlight added line
  lineNumberRef.current?.setLineColor(1, "#1a4d1a")
  lineNumberRef.current?.setLineSign(1, { after: " +", afterColor: "#22c55e" })
  
  // Highlight deleted line
  lineNumberRef.current?.setLineColor(5, "#4d1a1a")
  lineNumberRef.current?.setLineSign(5, { after: " -", afterColor: "#ef4444" })
  
  // Add diagnostic warning
  lineNumberRef.current?.setLineSign(10, { before: "⚠️", beforeColor: "#f59e0b" })
}, [])

<line-number
  ref={lineNumberRef}
  fg="#6b7280"
  bg="#161b22"
  minWidth={3}
  showLineNumbers={true}
>
  <code content={codeContent} filetype="typescript" />
</line-number>

Diff Component

Unified or split diff viewer:

<diff
  oldContent={oldCode}
  newContent={newCode}
  filetype="typescript"
  syntaxStyle={syntaxStyle}
  viewMode="unified"           // unified | split
/>

ASCII Font Component

Display ASCII art text:

<ascii-font
  text="OPENTUI"
  font="block"                 // tiny | block | slick | shade
  color={RGBA.fromHex("#00FF00")}
/>

React Hooks

useRenderer()

Access the renderer instance:

import { useRenderer } from "@opentui/react"

const renderer = useRenderer()

// Show/hide console overlay
renderer.console.toggle()
renderer.console.show()
renderer.console.hide()

useKeyboard(handler, options?)

Handle keyboard events:

import { useKeyboard } from "@opentui/react"
import type { KeyEvent } from "@opentui/core"

useKeyboard((key: KeyEvent) => {
  console.log("Key:", key.name)
  console.log("Modifiers:", { ctrl: key.ctrl, shift: key.shift, alt: key.meta })
  
  if (key.name === "escape") {
    process.exit(0)
  }
  
  if (key.ctrl && key.name === "s") {
    // Save action
  }
}, { release: false })  // Set release: true for key release events

Key Event Properties:

  • name - Key name (e.g., "a", "enter", "escape", "up", "down")
  • ctrl - Ctrl key pressed
  • shift - Shift key pressed
  • meta - Alt/Meta key pressed (Linux/Windows)
  • option - Option key pressed (macOS)
  • sequence - Raw key sequence

useTerminalDimensions()

Get current terminal size (auto-updates on resize):

import { useTerminalDimensions } from "@opentui/react"

const { width, height } = useTerminalDimensions()

return (
  <box style={{ width, height }}>
    <text>{`Terminal: ${width}x${height}`}</text>
  </box>
)

useOnResize(callback)

Handle terminal resize events:

import { useOnResize } from "@opentui/react"

useOnResize((width, height) => {
  console.log(`Resized to ${width}x${height}`)
})

useTimeline(options?)

Create animations:

import { useTimeline } from "@opentui/react"
import { useEffect, useState } from "react"

const [width, setWidth] = useState(0)

const timeline = useTimeline({
  duration: 2000,
  loop: false,
  autoplay: true,
})

useEffect(() => {
  timeline.add(
    { width },
    {
      width: 50,
      duration: 2000,
      ease: "linear",
      onUpdate: (animation) => {
        setWidth(animation.targets[0].width)
      },
    }
  )
}, [])

return <box style={{ width }}><text>Animating...</text></box>

Common Patterns

Multi-view Application

import { useState } from "react"
import { useKeyboard } from "@opentui/react"

type View = "home" | "settings" | "help"

function App() {
  const [view, setView] = useState<View>("home")

  useKeyboard((key) => {
    if (key.name === "1") setView("home")
    if (key.name === "2") setView("settings")
    if (key.name === "3") setView("help")
    if (key.name === "escape") process.exit(0)
  })

  return (
    <box style={{ flexDirection: "column", width: "100%", height: "100%" }}>
      <Header view={view} />
      {view === "home" && <HomeView />}
      {view === "settings" && <SettingsView />}
      {view === "help" && <HelpView />}
      <Footer />
    </box>
  )
}

Modal Pattern

import { useState } from "react"
import { useKeyboard } from "@opentui/react"

function App() {
  const [showModal, setShowModal] = useState(false)

  useKeyboard((key) => {
    if (showModal) return  // Modal handles its own keys
    
    if (key.name === "m") {
      setShowModal(true)
    }
  })

  return (
    <>
      <MainContent />
      {showModal && (
        <Modal onClose={() => setShowModal(false)} />
      )}
    </>
  )
}

function Modal({ onClose }: { onClose: () => void }) {
  const [input, setInput] = useState("")
  
  useKeyboard((key) => {
    if (key.name === "escape") onClose()
  })

  return (
    <box
      style={{
        position: "absolute",
        top: "50%",
        left: "50%",
        width: 60,
        height: 10,
        backgroundColor: "#1a1a1a",
        border: true,
        borderColor: "#FFFFFF",
      }}
    >
      <input
        placeholder="Enter value..."
        value={input}
        focused={true}
        onInput={setInput}
        onSubmit={(val) => {
          console.log("Submitted:", val)
          onClose()
        }}
      />
    </box>
  )
}

Focus Management

import { useState } from "react"
import { useKeyboard } from "@opentui/react"

type FocusPanel = "left" | "right"

function App() {
  const [focused, setFocused] = useState<FocusPanel>("left")

  useKeyboard((key) => {
    if (key.name === "tab") {
      setFocused(focused === "left" ? "right" : "left")
    }
  })

  return (
    <box style={{ flexDirection: "row", width: "100%", height: "100%" }}>
      <box
        title="Left Panel"
        style={{ flexGrow: 1, border: true }}
        focused={focused === "left"}
      >
        <text>Content</text>
      </box>
      <box
        title="Right Panel"
        style={{ flexGrow: 1, border: true }}
        focused={focused === "right"}
      >
        <text>Content</text>
      </box>
    </box>
  )
}

List with Selection

import { useState } from "react"
import { useKeyboard } from "@opentui/react"

function FileList({ files }: { files: string[] }) {
  const [selected, setSelected] = useState(0)

  useKeyboard((key) => {
    if (key.name === "up") {
      setSelected(Math.max(0, selected - 1))
    }
    if (key.name === "down") {
      setSelected(Math.min(files.length - 1, selected + 1))
    }
    if (key.name === "enter") {
      console.log("Selected:", files[selected])
    }
  })

  return (
    <box style={{ flexDirection: "column" }}>
      {files.map((file, idx) => (
        <text
          key={file}
          fg={idx === selected ? "#00FF00" : "#FFFFFF"}
          bg={idx === selected ? "#333333" : undefined}
        >
          {idx === selected ? "> " : "  "}
          {file}
        </text>
      ))}
    </box>
  )
}

Responsive Layout

import { useTerminalDimensions } from "@opentui/react"

function App() {
  const { width, height } = useTerminalDimensions()
  
  // Show simplified layout on small terminals
  const isSmall = width < 80

  return (
    <box style={{ flexDirection: isSmall ? "column" : "row", width, height }}>
      <Sidebar width={isSmall ? width : 30} />
      <MainContent width={isSmall ? width : width - 30} />
    </box>
  )
}

Loading Spinner

import { useState, useEffect } from "react"

function Spinner() {
  const [frame, setFrame] = useState(0)
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

  useEffect(() => {
    const interval = setInterval(() => {
      setFrame((f) => (f + 1) % frames.length)
    }, 80)
    return () => clearInterval(interval)
  }, [])

  return <text fg="#00FFFF">{frames[frame]} Loading...</text>
}

Code Style Guidelines

Import Order

  1. External dependencies (React, etc.)
  2. OpenTUI packages
  3. Internal utilities
  4. Components
  5. Type imports (with type keyword)
import { useState, useEffect } from "react"
import { useKeyboard, useRenderer } from "@opentui/react"
import { RGBA, TextAttributes } from "@opentui/core"
import { formatTime } from "./utils"
import { Header } from "./components/Header"
import type { AppState, View } from "./types"

Naming Conventions

  • camelCase: variables, functions, parameters
  • PascalCase: components, classes, types/interfaces
  • UPPER_CASE: constants
// Good
const userInput = "text"
function handleSubmit() {}
interface UserData {}
const MAX_RETRIES = 3

// Bad
const UserInput = "text"
function HandleSubmit() {}
interface userData {}

Component Structure

interface ComponentProps {
  title: string
  items: string[]
}

export function Component({ title, items }: ComponentProps) {
  // 1. Hooks
  const [selected, setSelected] = useState(0)
  const renderer = useRenderer()
  
  // 2. Event handlers
  useKeyboard((key) => {
    if (key.name === "escape") process.exit(0)
  })
  
  // 3. Effects
  useEffect(() => {
    // Side effects
  }, [])
  
  // 4. Helper functions
  const handleSelect = (index: number) => {
    setSelected(index)
  }
  
  // 5. JSX return
  return (
    <box>
      <text>{title}</text>
      {/* Content */}
    </box>
  )
}

Best Practices

Performance

  1. Minimize re-renders: Use useMemo and useCallback for expensive computations
  2. Avoid frequent updates: Throttle or debounce rapid state changes
  3. Use keys properly: Provide stable keys for lists to optimize reconciliation
  4. Optimize large lists: Use virtualization or pagination for long lists

Focus Management

  1. Only one focused component: Ensure only one input/select has focused={true} at a time
  2. Keyboard routing: Check focus state before handling keyboard events
  3. Visual feedback: Use focused prop on boxes to show focus state with borders

Layout Tips

  1. Use flexbox: Leverage flexDirection, justifyContent, alignItems for layouts
  2. Percentage widths: Use "50%" for responsive layouts
  3. flexGrow/flexShrink: Use for responsive sizing
  4. Absolute positioning: Use sparingly for modals/overlays

Debugging

  1. Console overlay: Use renderer.console.show() instead of console.log
  2. Toggle console: Press backtick (`) to toggle console visibility
  3. Environment variables: Set OTUI_DEBUG=true for debug mode
  4. Stats overlay: Set OTUI_SHOW_STATS=true to see performance stats

Error Handling

  1. Graceful degradation: Handle terminal resize and small screens
  2. Input validation: Validate user input before processing
  3. Error boundaries: Wrap components in error boundaries for production

Common Gotchas

  1. Console captured: console.log goes to overlay, use renderer.console.show() to view
  2. Focus required: Inputs/selects need focused={true} to receive events
  3. Bun only: OpenTUI is designed for Bun runtime, not Node.js
  4. Zig for builds: Only needed when modifying native code, not for TypeScript changes
  5. Alternate screen: Default is full-screen mode, use useAlternateScreen: false to disable
  6. Key names: Use lowercase names ("a", "enter", "escape"), not "A", "Enter", "Escape"

Troubleshooting

Application not rendering

  • Ensure you called renderer.start() after createRoot().render()
  • Check terminal supports ANSI escape codes
  • Try disabling alternate screen: useAlternateScreen: false

Keyboard input not working

  • Ensure component has focused={true}
  • Check if modal or overlay is capturing input
  • Verify useKeyboard hook is registered

Layout issues

  • Check parent has explicit dimensions (width, height)
  • Use flexGrow for flexible sizing
  • Ensure flexDirection is set correctly
  • Use browser DevTools-like thinking (Yoga = Flexbox)

Colors not showing

  • Verify terminal supports 24-bit color
  • Use hex format: "#RRGGBB"
  • Check color values are valid (0-255 for RGB)

Reference Documentation

For detailed API references and advanced topics, see:

Examples

The OpenTUI repository contains many examples:

  • Basic counter app
  • Login form with validation
  • File explorer with keyboard navigation
  • Code viewer with syntax highlighting
  • Git diff viewer
  • Multi-view dashboard
  • Modal dialogs
  • Tab navigation

Resources

Development Commands

# Install dependencies
bun install

# Run application
bun run index.tsx

# Type check
bun --bun tsc --noEmit

# Build project (if applicable)
bun run build

Next Steps

After implementing a basic OpenTUI application:

  1. Add keyboard shortcuts: Implement common shortcuts (Ctrl+Q, etc.)
  2. Create themes: Extract colors to theme objects for consistency
  3. Add help screen: Show keyboard shortcuts and commands
  4. Implement error handling: Gracefully handle edge cases
  5. Optimize performance: Profile and optimize render performance
  6. Add tests: Write unit tests for components and logic
  7. Document usage: Add README with screenshots and usage guide

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

82/100Analyzed 2/23/2026

Comprehensive OpenTUI development skill covering framework fundamentals, core concepts (renderer, layout, colors), and extensive React component library. Well-structured with code examples throughout. Includes 'when to use' section and useful metadata. Score slightly reduced due to truncated content (useKeyboard hook cut off), missing sections (troubleshooting, testing), and somewhat mismatched tags (api, ci-cd, github for a TUI framework). Overall high-quality technical reference."

95
85
85
78
75

Metadata

Licenseunknown
Version-
Updated2/8/2026
Publishergitarbor

Tags

apici-cdgithub