home / skills / dimitrigilbert / ai-skills / opentui-react

opentui-react skill

/opentui-react

This skill assists building OpenTUI React components, hooks, and patterns, enabling efficient UI, state, styling, and testing workflows.

npx playbooks add skill dimitrigilbert/ai-skills --skill opentui-react

Review the files below or copy the command above to add this skill to your agents.

Files (6)
SKILL.md
9.3 KB
---
name: opentui-react
description: Expert assistance for OpenTUI with React. Use for React components, hooks (useKeyboard, useRenderer, useTimeline), JSX patterns, state management, forms, and testing.
---

# OpenTUI React Integration

Expert assistance for building terminal UIs with OpenTUI and React.

## Quick Start

```bash
# Install dependencies
bun install @opentui/core @opentui/react react
```

## Basic Setup

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

function App() {
  return <text>Hello, OpenTUI React!</text>
}

async function main() {
  const renderer = await createCliRenderer()
  createRoot(renderer).render(<App />)
}

main()
```

## React Hooks

### useKeyboard

Handle keyboard events in React components.

```tsx
import { useKeyboard } from "@opentui/react"

function App() {
  useKeyboard((key) => {
    if (key.name === "c" && key.ctrl) {
      process.exit(0)
    }
    if (key.name === "q") {
      process.exit(0)
    }
  })

  return <text>Press Ctrl+C or q to exit</text>
}
```

### useRenderer

Access the renderer instance.

```tsx
import { useRenderer } from "@opentui/react"

function Component() {
  const renderer = useRenderer()

  const handleClick = () => {
    console.log("Renderer available:", !!renderer)
  }

  return <box onClick={handleClick}>Click me</box>
}
```

### useTerminalDimensions

Get terminal size changes.

```tsx
import { useTerminalDimensions } from "@opentui/react"

function Responsive() {
  const { width, height } = useTerminalDimensions()

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

### useTimeline

Create animations in React.

```tsx
import { useTimeline } from "@opentui/react"
import { useRef } from "react"

function AnimatedBox() {
  const boxRef = useRef<any>(null)

  const timeline = useTimeline({
    duration: 1000,
    easing: (t) => t * (2 - t), // easeOutQuad
  })

  const animate = () => {
    if (boxRef.current) {
      timeline.to(boxRef.current, {
        backgroundColor: { r: 255, g: 0, b: 0 },
      })
      timeline.play()
    }
  }

  return (
    <box ref={boxRef} onClick={animate}>
      <text>Click to animate</text>
    </box>
  )
}
```

## React Components

All OpenTUI components are available as JSX elements:

```tsx
import {
  text,
  box,
  input,
  select,
  scrollbox,
  code,
} from "@opentui/react"

function Form() {
  return (
    <box flexDirection="column" gap={1}>
      <text decoration="bold">User Information</text>

      <input placeholder="Name" />
      <input placeholder="Email" />

      <select
        options={[
          { label: "Option 1", value: "1" },
          { label: "Option 2", value: "2" },
        ]}
      />

      <box borderStyle="single">
        <text>Submit</text>
      </box>
    </box>
  )
}
```

## Styling in React

Styles are passed as props to components:

```tsx
function StyledComponent() {
  return (
    <box
      borderStyle="double"
      borderColor={{ r: 100, g: 149, b: 237 }}
      backgroundColor={{ r: 30, g: 30, b: 30 }}
      padding={1}
    >
      <text
        foregroundColor={{ r: 255, g: 255, b: 255 }}
        decoration="bold underline"
      >
        Styled Text
      </text>
    </box>
  )
}
```

**Color format:** `{ r: number, g: number, b: number, a?: number }`

## State Management

### Local State

```tsx
import { useState } from "react"

function Counter() {
  const [count, setCount] = useState(0)

  useKeyboard((key) => {
    if (key.name === "up") setCount(c => c + 1)
    if (key.name === "down") setCount(c => c - 1)
  })

  return (
    <box>
      <text>Count: {count}</text>
      <text>Use arrow keys</text>
    </box>
  )
}
```

### Form State

```tsx
function LoginForm() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const [errors, setErrors] = useState<any>({})

  const handleSubmit = () => {
    const newErrors: any = {}

    if (!email.includes("@")) {
      newErrors.email = "Invalid email"
    }
    if (password.length < 8) {
      newErrors.password = "Password too short"
    }

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors)
      return
    }

    console.log("Login:", { email, password })
  }

  return (
    <box flexDirection="column" gap={1}>
      <text decoration="bold">Login</text>

      <input
        value={email}
        onChange={setEmail}
        placeholder="Email"
      />
      {errors.email && (
        <text foregroundColor={{ r: 231, g: 76, b: 60 }}>
          {errors.email}
        </text>
      )}

      <input
        value={password}
        onChange={setPassword}
        placeholder="Password"
        password
      />
      {errors.password && (
        <text foregroundColor={{ r: 231, g: 76, b: 60 }}>
          {errors.password}
        </text>
      )}

      <box onClick={handleSubmit} borderStyle="single">
        <text>Submit</text>
      </box>
    </box>
  )
}
```

### External State Management

#### Redux Integration

```tsx
import { Provider, useSelector, useDispatch } from "react-redux"

function Counter() {
  const count = useSelector((state: any) => state.count)
  const dispatch = useDispatch()

  useKeyboard((key) => {
    if (key.name === "up") dispatch({ type: "INCREMENT" })
    if (key.name === "down") dispatch({ type: "DECREMENT" })
  })

  return <text>Count: {count}</text>
}
```

#### Zustand Integration

```tsx
import { create } from "zustand"

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state: any) => ({ count: state.count + 1 })),
  decrement: () => set((state: any) => ({ count: state.count - 1 })),
}))

function Counter() {
  const { count, increment, decrement } = useStore()

  useKeyboard((key) => {
    if (key.name === "up") increment()
    if (key.name === "down") decrement()
  })

  return <text>Count: {count}</text>
}
```

## Common Patterns

### List with Selection

```tsx
function SelectList({ items }: { items: string[] }) {
  const [selectedIndex, setSelectedIndex] = useState(0)

  useKeyboard((key) => {
    if (key.name === "down" || (key.name === "tab" && !key.shift)) {
      setSelectedIndex(i => Math.min(i + 1, items.length - 1))
    }
    if (key.name === "up" || (key.name === "tab" && key.shift)) {
      setSelectedIndex(i => Math.max(i - 1, 0))
    }
    if (key.name === "enter") {
      console.log("Selected:", items[selectedIndex])
    }
  })

  return (
    <scrollbox height={20}>
      {items.map((item, index) => (
        <box
          key={index}
          backgroundColor={
            index === selectedIndex
              ? { r: 100, g: 149, b: 237 }
              : { r: 30, g: 30, b: 30 }
          }
        >
          <text
            foregroundColor={
              index === selectedIndex
                ? { r: 255, g: 255, b: 255 }
                : { r: 255, g: 255, b: 255 }
            }
          >
            {index === selectedIndex ? "> " : "  "}{item}
          </text>
        </box>
      ))}
    </scrollbox>
  )
}
```

### Tabs

```tsx
function Tabs({ tabs }: { tabs: Array<{ id: string, label: string, content: any }> }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id)

  return (
    <box flexDirection="column" height={30}>
      {/* Tab headers */}
      <box flexDirection="row">
        {tabs.map(tab => (
          <box
            key={tab.id}
            onClick={() => setActiveTab(tab.id)}
            borderStyle={activeTab === tab.id ? "single" : "none"}
            backgroundColor={
              activeTab === tab.id
                ? { r: 100, g: 149, b: 237 }
                : { r: 50, g: 50, b: 50 }
            }
            padding={1}
          >
            <text>{tab.label}</text>
          </box>
        ))}
      </box>

      {/* Tab content */}
      <box flexGrow={1} padding={1}>
        {tabs.find(t => t.id === activeTab)?.content}
      </box>
    </box>
  )
}
```

### Modal/Dialog

```tsx
function Modal({ isOpen, onClose, children }: any) {
  if (!isOpen) return null

  return (
    <box
      position="absolute"
      top={0}
      left={0}
      width="100%"
      height="100%"
      backgroundColor={{ r: 0, g: 0, b: 0, a: 0.5 }}
      justifyContent="center"
      alignItems="center"
      onClick={onClose}
    >
      <box
        borderStyle="double"
        backgroundColor={{ r: 30, g: 30, b: 30 }}
        padding={2}
        onClick={(e: any) => e.stopPropagation()}
      >
        {children}
      </box>
    </box>
  )
}
```

## When to Use This Skill

Use `/opentui-react` for:
- Building TUIs with React
- Using hooks (useKeyboard, useRenderer, etc.)
- JSX-style component development
- Integrating with React state management
- Testing React OpenTUI components

For vanilla TypeScript/JavaScript, use `/opentui`
For SolidJS development, use `/opentui-solid`
For project scaffolding, use `/opentui-projects`

## Resources

- [Hooks Reference](references/HOOKS.md) - Complete hook documentation
- [Component Props](references/PROPS.md) - React component props
- [Patterns](references/PATTERNS.md) - Common React patterns
- [State Management](references/STATE.md) - React state integration
- [Testing](references/TESTING.md) - Testing React components

## Key Knowledge Sources

- OpenTUI React GitHub: https://github.com/sst/opentui/tree/main/packages/react
- Context7: `/sst/opentui` - React integration queries
- Research: `.search-data/research/opentui/`

Overview

This skill provides expert guidance for building terminal user interfaces (TUIs) with OpenTUI and React. It focuses on React-specific patterns: components as JSX elements, hooks for input and rendering, animations, styling, and state integration. Use it to accelerate development of interactive, accessible terminal apps with TypeScript and React.

How this skill works

I explain how to wire OpenTUI into a React app, create a renderer, and mount a root. The skill documents hooks like useKeyboard, useRenderer, useTerminalDimensions, and useTimeline, showing how to respond to input, access the renderer, track terminal size, and animate elements. It covers component props, color format, styling via props, and examples for local, form, and external state management.

When to use it

  • Building interactive terminal UIs using React and TypeScript
  • Handling keyboard input, focus, and selection in TUI components
  • Animating components or responding to terminal resize events
  • Integrating OpenTUI UI with Redux, Zustand, or other state stores
  • Creating forms, lists, tabs, modals, and other common TUI patterns

Best practices

  • Create a single renderer with createCliRenderer and mount one root via createRoot
  • Use useKeyboard for global key handling and keep handlers idempotent
  • Manage local UI state with useState; lift shared state to Redux/Zustand when needed
  • Use numeric RGB(A) color objects for consistent styling across components
  • Keep event handlers lightweight; avoid blocking operations in render paths
  • Stop propagation inside modal content to prevent accidental background clicks

Example use cases

  • A keyboard-driven file navigator with scrollbox and selection using arrow keys
  • A login form with validation, error display, and submit handling via input components
  • Tabbed interfaces where headers switch content and active tab is stored in state
  • Animated status or progress indicators using useTimeline for smooth transitions
  • Embedding OpenTUI components inside a Redux/Zustand-powered app for shared state

FAQ

How do I exit the app with keyboard shortcuts?

Use useKeyboard and call process.exit(0) on keys like Ctrl+C or 'q'. Keep exit logic centralized.

Can I animate component styles?

Yes. Use useTimeline to animate props like backgroundColor. Target refs to elements and call timeline.to(...).play().