All skills
msmps avatar

/opentui

@0d61d6d
by Matt Simpsonmsmps/opentui-skill225 stars
9

OpenTUI skill for building terminal user interfaces with the Core, React, or Solid APIs. Use for any TUI task including components, layout, keyboard and keymap handling, animations, and testing.

Use this Skill: https://skilld.dev/gh/msmps/opentui-skill/opentui

This session only. Nothing lands on disk.

referencesanimationREFERENCE.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Animation System

OpenTUI provides a timeline-based animation system for smooth property transitions.

Overview

Animations in OpenTUI use:

  • Timeline: Orchestrates multiple animations
  • Animation Engine: Manages timelines and rendering
  • Easing Functions: Control animation curves

When to Use

Use this reference when you need timeline-driven animations, easing curves, or progressive transitions.

Basic Usage

React

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

function AnimatedBox() {
  const [width, setWidth] = useState(0)
  
  const timeline = useTimeline({
    duration: 2000,
  })
  
  useEffect(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "outQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  }, [])
  
  return (
    <box
      width={width}
      height={3}
      backgroundColor="#6a5acd"
    />
  )
}

Solid

import { useTimeline } from "@opentui/solid"
import { createSignal, onMount } from "solid-js"

function AnimatedBox() {
  const [width, setWidth] = createSignal(0)
  
  const timeline = useTimeline({
    duration: 2000,
  })
  
  onMount(() => {
    timeline.add(
      { width: 0 },
      {
        width: 50,
        duration: 2000,
        ease: "outQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].width))
        },
      }
    )
  })
  
  return (
    <box
      width={width()}
      height={3}
      backgroundColor="#6a5acd"
    />
  )
}

Core

import { createCliRenderer, Timeline, engine } from "@opentui/core"

const renderer = await createCliRenderer()
engine.attach(renderer)

const timeline = new Timeline({
  duration: 2000,
  autoplay: true,
})

timeline.add(
  { x: 0 },
  {
    x: 50,
    duration: 2000,
    ease: "outQuad",
    onUpdate: (anim) => {
      box.left = Math.round(anim.targets[0].x)
    },
  }
)

engine.register(timeline)

Timeline Options

const timeline = useTimeline({
  duration: 2000,         // Total duration in ms
  loop: false,            // Loop the timeline
  autoplay: true,         // Start automatically
  onComplete: () => {},   // Called when timeline completes
  onPause: () => {},      // Called when timeline pauses
})

Timeline Methods

// Add animation
timeline.add(target, properties, startTime?)

// Control playback
timeline.play()           // Start/resume
timeline.pause()          // Pause
timeline.restart()        // Restart from beginning

// State
timeline.currentTime      // Current position in milliseconds
timeline.duration         // Total duration

Animation Properties

timeline.add(
  { value: 0 },           // Target object with initial values
  {
    value: 100,           // Final value
    duration: 1000,       // Animation duration in ms
    ease: "linear",       // Easing function
    onUpdate: (anim) => {
      // Called each frame
      const current = anim.targets[0].value
    },
    onComplete: () => {
      // Called when this animation completes
    },
  },
  0                       // Start time in timeline; use this instead of delay
)

Easing Functions

Available easing functions:

Linear

Name Description
linear Constant speed

Curves

Family Names
Quad inQuad, outQuad, inOutQuad
Expo inExpo, outExpo
Sine inOutSine
Circular inCirc, outCirc, inOutCirc
Back inBack, outBack, inOutBack
Bounce inBounce, outBounce
Elastic outElastic

Patterns

Progress Bar

function ProgressBar({ progress }: { progress: number }) {
  const [width, setWidth] = useState(0)
  const maxWidth = 50
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { value: width },
      {
        value: (progress / 100) * maxWidth,
        duration: 300,
        ease: "outQuad",
        onUpdate: (anim) => {
          setWidth(Math.round(anim.targets[0].value))
        },
      }
    )
  }, [progress])
  
  return (
    <box flexDirection="column" gap={1}>
      <text>Progress: {progress}%</text>
      <box width={maxWidth} height={1} backgroundColor="#333">
        <box width={width} height={1} backgroundColor="#00FF00" />
      </box>
    </box>
  )
}

Fade In

function FadeIn({ children }) {
  const [opacity, setOpacity] = useState(0)
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { opacity: 0 },
      {
        opacity: 1,
        duration: 500,
        ease: "outQuad",
        onUpdate: (anim) => {
          setOpacity(anim.targets[0].opacity)
        },
      }
    )
  }, [])
  
  return (
    <box style={{ opacity }}>
      {children}
    </box>
  )
}

Looping Animation

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>{frames[frame]} Loading...</text>
}

Staggered Animation

function StaggeredList({ items }) {
  const [visibleCount, setVisibleCount] = useState(0)
  
  useEffect(() => {
    let count = 0
    const interval = setInterval(() => {
      count++
      setVisibleCount(count)
      if (count >= items.length) {
        clearInterval(interval)
      }
    }, 100)
    
    return () => clearInterval(interval)
  }, [items.length])
  
  return (
    <box flexDirection="column">
      {items.slice(0, visibleCount).map((item, i) => (
        <text key={i}>{item}</text>
      ))}
    </box>
  )
}

Slide In

function SlideIn({ children, from = "left" }) {
  const [offset, setOffset] = useState(from === "left" ? -20 : 20)
  
  const timeline = useTimeline()
  
  useEffect(() => {
    timeline.add(
      { offset: from === "left" ? -20 : 20 },
      {
        offset: 0,
        duration: 300,
        ease: "outQuad",
        onUpdate: (anim) => {
          setOffset(Math.round(anim.targets[0].offset))
        },
      }
    )
  }, [])
  
  return (
    <box position="relative" left={offset}>
      {children}
    </box>
  )
}

Performance Tips

Batch Updates

Timeline automatically batches updates within the render loop.

Use Integer Values

Round animated values for character-based positioning:

onUpdate: (anim) => {
  setX(Math.round(anim.targets[0].x))
}

Clean Up Timelines

Hooks automatically clean up, but for core:

// When done with timeline
engine.unregister(timeline)

Gotchas

Terminal Refresh Rate

Terminal UIs typically refresh at 60 FPS max. Very fast animations may appear choppy.

Character Grid

Animations are constrained to character cells. Sub-pixel positioning isn't possible.

Cleanup in Effects

Always clean up intervals and timelines:

useEffect(() => {
  const interval = setInterval(...)
  return () => clearInterval(interval)
}, [])

See Also

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    This skill contains documentation and usage patterns for the OpenTUI framework, designed for building terminal user interfaces across Core, React, and SolidJS. The analysis confirmed that the skill provides legitimate development guidance; no malicious code, persistence mechanisms, or obfuscation were detected. Findings are limited to standard platform capabilities and recommended scaffolding tools.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    1/26 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 0d61d6d. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated last month
Other metadata
metadata
{
  "references": "core, react, solid, components, layout, keyboard, keymap, animation, testing"
}
  • React
  • Testing
  • terminal-ui
  • tui
  • solid
  • components
  • keyboard-input
  • animations
  • layout

README badge

README badge for msmps/opentui-skill

Builds terminal user interfaces with OpenTUI using an imperative core API or React/Solid reconcilers. Covers layout, keyboard handling, animations, components, and testing for full-featured TUI applications.

Generated from the current SKILL.md.

Does this skill cover all three OpenTUI frameworks?
Yes. The skill consolidates the core imperative API, React reconciler, and Solid reconciler. Use the decision tree in the skill to pick the right framework for your task.
How do I start a new OpenTUI project?
Use `create-tui` with options before arguments: `bunx create-tui -t react my-app` (not `bunx create-tui my-app -t react`). Then read the REFERENCE.md for your chosen framework.
What runtime does OpenTUI require?
OpenTUI runs on Bun and uses Zig for native builds. See `./references/core/gotchas.md` for runtime requirements and build guidance.
Can I use `process.exit()` to shut down my TUI?
No. Always use `renderer.destroy()` instead. Calling `process.exit()` directly is a common pitfall covered in `core/gotchas.md`.
How do I apply text styling in React or Solid?
Use nested modifier elements, not props. See `components/text-display.md` for the correct pattern.

Generated from the current SKILL.md. These answers refresh after source changes.