원클릭으로
opentui-react
OpenTUI with React — components, hooks (useKeyboard, useRenderer, useTimeline), JSX patterns, state, forms, testing.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
OpenTUI with React — components, hooks (useKeyboard, useRenderer, useTimeline), JSX patterns, state, forms, testing.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Create, verify, and improve AGENTS.md files. Minimal, focused, progressive disclosure. Fixes bloat, contradictions, stale info.
Audit and plan website optimisation for AI agents, AI search, LLM discoverability, llms.txt, structured data, and sitemaps.
Build distinctive, production-grade frontend interfaces (websites, components, dashboards, layouts) with polished UI design that avoids generic AI aesthetics.
Generate and validate Awesome list READMEs following sindresorhus/awesome standards.
Scaffold type-safe TypeScript projects with the Better-T-Stack CLI — new projects, features, or troubleshooting.
Build full-stack TypeScript apps with Convex — server functions, schema, auth, file storage, real-time, frontend integration, testing, deployment.
| name | opentui-react |
| description | OpenTUI with React — components, hooks (useKeyboard, useRenderer, useTimeline), JSX patterns, state, forms, testing. |
Expert assistance for building terminal UIs with OpenTUI and React.
# Install dependencies
bun install @opentui/core @opentui/react react
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()
Handle keyboard events in React components.
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>
}
Access the renderer instance.
import { useRenderer } from "@opentui/react"
function Component() {
const renderer = useRenderer()
const handleClick = () => {
console.log("Renderer available:", !!renderer)
}
return <box onClick={handleClick}>Click me</box>
}
Get terminal size changes.
import { useTerminalDimensions } from "@opentui/react"
function Responsive() {
const { width, height } = useTerminalDimensions()
return (
<box>
<text>Terminal: {width}x{height}</text>
</box>
)
}
Create animations in React.
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>
)
}
All OpenTUI components are available as JSX elements:
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>
)
}
Styles are passed as props to components:
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 }
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>
)
}
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>
)
}
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>
}
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>
}
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>
)
}
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>
)
}
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>
)
}
Use /opentui-react for:
For vanilla TypeScript/JavaScript, use /opentui
For SolidJS development, use /opentui-solid
For project scaffolding, use /opentui-projects
/sst/opentui - React integration queries.search-data/research/opentui/