npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@unblessed/react

v1.0.0-alpha.23

Published

React renderer for unblessed with flexbox layout support

Readme

@unblessed/react

React renderer for unblessed with flexbox layout support.

npm version License: MIT

⚠️ ALPHA SOFTWARE / WORK IN PROGRESS - This package is under active development. Core functionality is implemented but not all features are complete yet.

Overview

@unblessed/react enables building terminal UIs using React components and JSX, with automatic flexbox layout powered by Yoga.

Features:

  • 🎨 React components - Build UIs with familiar JSX syntax
  • 📦 Flexbox layout - Automatic positioning via Yoga layout engine
  • 🎯 TypeScript first - Full type safety
  • 🌐 Platform-agnostic - Works in Node.js and browsers
  • 🖱️ Event handling - onClick, onKeyPress, onSubmit, and more
  • 🎭 Theme system - Runtime-swappable themes with design tokens
  • State styling - Hover and focus effects with theme integration

Installation

npm install @unblessed/react@alpha react
# or
pnpm add @unblessed/react@alpha react

Also install a runtime:

# For Node.js
npm install @unblessed/node@alpha

# For browser
npm install @unblessed/browser@alpha

Requirements:

  • Node.js >= 22.0.0
  • React ^18.0.0 or ^19.0.0

Quick Start

import { render, Box, Text, Spacer } from "@unblessed/react";

const App = () => (
  <Box flexDirection="column" padding={1} gap={1}>
    <Box border={1} borderStyle="single" padding={1}>
      <Text color="green" bold>
        Hello from React!
      </Text>
    </Box>

    <Box flexDirection="row" gap={2}>
      <Box width={20}>Left</Box>
      <Spacer />
      <Box width={20}>Right</Box>
    </Box>
  </Box>
);

render(<App />);

Note: The runtime (@unblessed/node or @unblessed/browser) auto-initializes when imported, so you don't need explicit initialization.

Important: Borders require border={1} (or borderTop={1}, etc.) so that Yoga reserves space for them. Without this, only borderStyle and borderColor won't work properly.

Components

Box

Container component with flexbox layout support.

<Box
  flexDirection="row" // Layout direction
  gap={2} // Gap between children
  padding={1} // Padding
  border={1} // Enable border (required for Yoga layout)
  borderStyle="single" // Border style
  borderColor="cyan" // Border color
  width={40} // Fixed width
  flexGrow={1} // Grow to fill space
>
  {/* children */}
</Box>

Flexbox Props:

  • flexDirection: 'row' | 'column' | 'row-reverse' | 'column-reverse'
  • justifyContent: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'
  • alignItems: 'flex-start' | 'center' | 'flex-end' | 'stretch'
  • flexGrow, flexShrink, flexBasis
  • width, height, minWidth, minHeight, maxWidth, maxHeight
  • padding, paddingX, paddingY, paddingTop, etc.
  • margin, marginX, marginY, marginTop, etc.
  • gap, rowGap, columnGap

Styling Props:

  • borderStyle: 'single' | 'double' | 'round' | 'bold' | 'classic'
  • borderColor, borderTopColor, borderBottomColor, etc.
  • color - Text color
  • backgroundColor - Background color

Text

Text component with styling support.

<Text
  color="green" // Text color
  bold // Bold text
  italic // Italic text
  dim // Dim text
>
  Hello World!
</Text>

Props:

  • color, backgroundColor
  • bold, italic, underline, strikethrough
  • inverse, dim
  • textWrap - Text truncation mode (see below)

Text Wrapping:

// Truncate at end with ellipsis (ink-style)
<Text textWrap="truncate-end" width={20}>
  This is a very long line that will be truncated
</Text>
// Result: "This is a very lo…"

// Truncate in middle
<Text textWrap="truncate-middle" width={20}>
  This is a very long line
</Text>
// Result: "This is a…ng line"

// Truncate at start
<Text textWrap="truncate-start" width={20}>
  This is a very long line
</Text>
// Result: "…a very long line"

// Traditional word wrapping
<Text textWrap="wrap" width={20}>
  This is a very long line
</Text>
// Wraps to multiple lines

Available modes:

  • wrap - Word wrap to multiple lines (default)
  • truncate-end - Truncate at end with ellipsis
  • truncate-middle - Truncate in middle with ellipsis
  • truncate-start - Truncate at start with ellipsis

Spacer

Flexible space component that expands to fill available space.

<Box flexDirection="row">
  <Box width={20}>Left</Box>
  <Spacer /> {/* Fills remaining space */}
  <Box width={20}>Right</Box>
</Box>

Equivalent to <Box flexGrow={1} />.

BigText

Large ASCII art text component using terminal fonts.

<BigText color="cyan">HELLO</BigText>

Props:

  • color - Text color
  • backgroundColor - Background color

Automatically calculates dimensions based on font (14 rows × 8 columns per character).

Button

Interactive button component with hover and focus effects.

<Button
  padding={1}
  hover={{ bg: "blue", border: { color: "cyan" } }}
  focus={{ border: { color: "yellow" } }}
  onClick={() => console.log("Clicked!")}
  onPress={() => console.log("Pressed!")}
>
  Click Me
</Button>

Props:

  • All Box props (flexbox, border, colors, events, etc.)
  • hover - Hover state styling (colors, border, text styles)
  • focus - Focus state styling (colors, border, text styles)
  • onClick - Click handler
  • onPress - Press handler (Enter key or click)
  • tabIndex - Focus order (default: 0)

Theme Integration:

  • Automatically applies theme.components.button.hoverFg/hoverBg if no hover prop provided
  • User props override theme defaults

Input

Text input component for user interaction.

<Input
  width={40}
  value={text}
  onChange={(newValue) => setText(newValue)}
  onSubmit={(value) => console.log("Submitted:", value)}
  onCancel={() => console.log("Cancelled")}
/>

Props:

  • All Box props (flexbox, border, colors, events, etc.)
  • value - Controlled value
  • defaultValue - Uncontrolled default value
  • hover, focus - State styling
  • onSubmit - Submit handler (Enter key)
  • onCancel - Cancel handler (Escape key)
  • All Box events (onClick, onKeyPress, onFocus, onBlur, etc.)
  • tabIndex - Focus order (default: 0)

List

Scrollable, selectable list with keyboard and mouse support.

<List
  items={["Apple", "Banana", "Cherry"]}
  label="Choose a fruit"
  width={40}
  height={10}
  itemStyle={{ fg: "white" }}
  itemSelected={{ fg: "cyan", bold: true }}
  itemHover={{ fg: "yellow", bg: "black" }}
  onSelect={(item, index) => console.log("Selected:", item)}
/>

Props:

  • All Box props (container styling, borders, hover, focus)
  • items - Array of strings to display
  • label - List label/title
  • itemStyle - Normal item appearance
  • itemSelected - Selected item styling
  • itemHover - Hovered item styling
  • onSelect - Selection handler
  • onCancel - Cancel handler (Escape key)
  • disabled - Disable interaction
  • vi - Enable vi keybindings (j/k)
  • scrollbar, scrollbarFg, scrollbarBg - Scrollbar styling
  • tabIndex - Focus order (default: 0)

Theme Integration:

  • Container uses theme from Box (borders, hover, focus)
  • Items use theme.components.list.item.{fg,bg,selectedFg,selectedBg,hoverFg,hoverBg}

Note: Inherits all Box properties for container styling. Item styling uses separate props.

Examples

Dashboard Layout

const Dashboard = () => (
  <Box flexDirection="column" width={80} height={24}>
    {/* Header */}
    <Box height={3} border={1} borderStyle="single">
      <Text bold>Dashboard</Text>
    </Box>

    {/* Content area */}
    <Box flexDirection="row" flexGrow={1}>
      {/* Sidebar */}
      <Box width={20} border={1} borderStyle="single">
        <Text>Menu</Text>
      </Box>

      {/* Main content */}
      <Box flexGrow={1} border={1} borderStyle="single">
        <Text>Content area</Text>
      </Box>
    </Box>
  </Box>
);

render(<Dashboard />);

Centered Content

const Centered = () => (
  <Box justifyContent="center" alignItems="center" width={80} height={24}>
    <Box border={1} borderStyle="double" padding={2}>
      <Text color="cyan" bold>
        Centered!
      </Text>
    </Box>
  </Box>
);

Interactive Form

import { BigText, Box, Text, Button, Input } from "@unblessed/react";

const LoginForm = () => (
  <Box flexDirection="column" padding={2} gap={1}>
    <BigText color="cyan">LOGIN</BigText>

    <Box flexDirection="column" gap={1}>
      <Text>Username:</Text>
      <Input border={1} borderColor="blue" />

      <Text>Password:</Text>
      <Input border={1} borderColor="blue" />

      <Button border={1} hoverBg="green" focusBg="cyan" padding={1}>
        <Text>Login</Text>
      </Button>
    </Box>
  </Box>
);

Event Handling

All components support React-style event handlers that are automatically bound to unblessed widget events.

Mouse Events

<Box
  onClick={(data) => {
    console.log(`Clicked at ${data.x}, ${data.y}`);
  }}
  onMouseMove={(data) => {
    console.log(`Mouse moved to ${data.x}, ${data.y}`);
  }}
>
  Interactive Box
</Box>

Available:

  • onClick, onMouseDown, onMouseUp
  • onMouseMove, onMouseOver, onMouseOut
  • onMouseWheel, onWheelDown, onWheelUp

Keyboard Events

<Box
  onKeyPress={(ch, key) => {
    if (key.name === "enter") {
      console.log("Enter pressed!");
    }
    if (key.ctrl && key.name === "c") {
      process.exit(0);
    }
  }}
>
  Press keys
</Box>

Focus Events

<Button
  onFocus={() => console.log("Button focused")}
  onBlur={() => console.log("Button blurred")}
>
  Focus me
</Button>

Widget-Specific Events

Button:

<Button onPress={() => console.log("Button pressed!")}>Submit</Button>

Input:

<Input
  onSubmit={(value) => console.log("Submitted:", value)}
  onCancel={() => console.log("Cancelled")}
  onKeyPress={(ch, key) => console.log("Key:", key.name)}
/>

Interactive Example

import React, { useState } from "react";
import { render, Box, Text, Button, Input } from "@unblessed/react";

const Counter = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("");

  return (
    <Box flexDirection="column" gap={1} padding={2}>
      <Box border={1} borderStyle="single" padding={1}>
        <Text>Count: {count}</Text>
      </Box>

      <Button
        border={1}
        borderStyle="single"
        borderColor="green"
        padding={1}
        onClick={() => setCount((c) => c + 1)}
        onPress={() => setCount((c) => c + 1)}
      >
        Increment
      </Button>

      <Input
        border={1}
        borderColor="blue"
        onSubmit={(value) => {
          setName(value);
          console.log("Name set to:", value);
        }}
      />

      {name && (
        <Box padding={1}>
          <Text>Hello, {name}!</Text>
        </Box>
      )}
    </Box>
  );
};

render(<Counter />);

Theme System

@unblessed/react includes a comprehensive theme system with design tokens, runtime theme switching, and automatic terminal color reduction.

Quick Start

import { render, unblessedTheme, matrixTheme } from "@unblessed/react";

// Use default theme
render(<App />, { theme: unblessedTheme });

// Or use Matrix theme
render(<App />, { theme: matrixTheme });

Available Themes

unblessedTheme - Professional dark theme (default)

  • Background: #111827 (gray.900)
  • Foreground: #e5e7eb (gray.200)
  • Primary: #3b82f6 (blue.500)
  • Based on Tailwind CSS color system

matrixTheme - Matrix movie-inspired theme

  • Background: #0a0e0a (near-black with green tint)
  • Foreground: #00ff41 (bright Matrix green)
  • Primary: #00ff00 (classic Matrix green)

Runtime Theme Switching

import { useTheme, unblessedTheme, matrixTheme } from "@unblessed/react";

function App() {
  const [theme, setTheme] = useTheme();

  return (
    <Box flexDirection="column" gap={1}>
      <Button
        onClick={() =>
          setTheme(theme === unblessedTheme ? matrixTheme : unblessedTheme)
        }
      >
        Toggle Theme
      </Button>
      <Text fg="$primary">Primary colored text</Text>
    </Box>
  );
}

Theme Color References

Use $ prefix to reference theme colors:

// Semantic colors (recommended)
<Text fg="$primary">Primary text</Text>
<Text fg="$semantic.success">Success message</Text>
<Box borderColor="$semantic.border">Themed border</Box>

// Primitive colors (specific shades)
<Box bg="$primitives.blue.500">Blue background</Box>

// Explicit colors (theme-independent)
<Text fg="cyan">Always cyan, ignores theme</Text>

State Styling (Hover & Focus)

All interactive components support hover and focus styling:

<Button
  // Default state
  color="white"
  bg="blue"
  border={1}
  borderColor="blue"
  // Hover state
  hover={{
    bg: "darkblue",
    border: { color: "cyan" },
  }}
  // Focus state (when tabbed to)
  focus={{
    border: { color: "yellow" },
  }}
>
  Interactive Button
</Button>

Automatic Focus Border:

  • If widget has border={1} and no focus style provided
  • Automatically applies theme.global.focusBorderColor on focus
  • Override by providing explicit focus prop

Theme Architecture

Three-layer design token system:

  1. Primitives - Color scales (gray.50-900, blue.50-900, etc.)
  2. Semantic - Intent-based colors (primary, success, error, etc.)
  3. Components - Widget-specific defaults (button.hoverFg, list.item.selectedBg, etc.)

Resolution Order (CSS-like cascade):

User Props (highest priority)
    ↓
Component Theme Defaults
    ↓
Global Theme Defaults
    ↓
Hardcoded Fallbacks (lowest priority)

Creating Custom Themes

import { Theme, unblessedTheme } from "@unblessed/react";

const myTheme: Theme = {
  name: "my-theme",
  primitives: unblessedTheme.primitives, // Reuse color scales
  semantic: {
    ...unblessedTheme.semantic,
    primary: "#ff6b6b", // Custom primary color
    success: "#51cf66",
  },
  global: {
    ...unblessedTheme.global,
    focusBorderColor: "$primary",
  },
  components: {
    ...unblessedTheme.components,
    button: {
      bg: "$primary",
      fg: "#ffffff",
      hoverBg: "#ff5252",
      hoverFg: "#ffffff",
    },
    list: {
      item: {
        fg: "$semantic.foreground",
        selectedFg: "$primary",
        selectedBg: "$primitives.gray.800",
        hoverFg: "$semantic.accent",
      },
    },
  },
};

render(<App />, { theme: myTheme });

See examples/theme-demo.tsx for a comprehensive demonstration.

Architecture

@unblessed/react builds on top of:

  • @unblessed/core - Widget library and terminal rendering
  • @unblessed/layout - Yoga flexbox layout engine
  • react-reconciler - React's custom renderer API

Widget Descriptor Pattern

The implementation uses a descriptor pattern for type-safe widget configuration:

// Each widget has a descriptor that encapsulates its behavior
class BoxDescriptor extends WidgetDescriptor<BoxProps> {
  get flexProps(); // Extract layout props for Yoga
  get widgetOptions(); // Extract visual/behavioral props for unblessed
  get eventHandlers(); // Extract event handlers
  createWidget(); // Create unblessed widget instance
  updateWidget(); // Update widget on re-render
}

Benefits:

  • ✅ Type-safe props per widget type
  • ✅ No string-based type discrimination
  • ✅ Easy to extend with new widgets
  • ✅ Composition via helper functions (buildBorder, buildTextStyles, etc.)

High-Level Flow

React Components
    ↓
React Reconciler (creates LayoutNodes)
    ↓
LayoutManager (Yoga calculations)
    ↓
unblessed Widgets (positioned)
    ↓
Terminal Rendering

Understanding the Node Types

The architecture uses several different "node" types, each with a specific responsibility:

1. YogaNode (from yoga-layout)

The low-level flexbox layout engine (C++/WASM). Calculates positions and sizes based on flexbox properties.

// Created internally - you don't use this directly
yogaNode.setWidth(100);
yogaNode.setHeight(50);
yogaNode.calculateLayout(); // Computes layout

2. LayoutNode (from @unblessed/layout)

Wraps YogaNode with metadata, bridging React props to Yoga calculations and unblessed widgets.

interface LayoutNode {
  type: string; // 'box', 'text', 'button', etc.
  yogaNode: YogaNode; // The Yoga layout engine
  props: FlexboxProps; // width, height, padding, gap, etc.
  widgetOptions: any; // border, colors, content, etc.
  widget?: Element; // The final unblessed widget
  eventHandlers?: Record<string, Function>; // Event handlers to bind
}

3. DOMNode (from @unblessed/react)

Virtual DOM element used by React reconciler. Wraps LayoutNode with React-specific data.

interface DOMNode {
  type: ElementType; // 'box', 'text', 'root'
  layoutNode: LayoutNode; // References the layout layer
  props: BoxProps; // React component props
  childNodes: AnyNode[]; // Virtual DOM children
}

4. Widget/Element (from @unblessed/core)

The actual terminal UI widget that renders to screen. Created after Yoga calculates layout.

// Box, Text, Button, etc.
new Box({
  top: 10, // From Yoga calculations
  left: 20,
  width: 50,
  height: 30,
  border: { type: "line" },
  content: "Hello",
});

Data Flow Example

When you write:

<Box padding={1} gap={2} borderColor="cyan" onClick={handleClick}>
  <Text>Hello</Text>
</Box>

Here's what happens:

1. React Component
   <Box padding={1} gap={2} borderColor="cyan" onClick={handleClick}>
      ↓
2. React Reconciler creates DOMNode
   DOMNode { type: 'box', props: {...}, layoutNode: LayoutNode }
      ↓
3. LayoutNode splits props
   LayoutNode {
     props: { padding: 1, gap: 2 },        → Sent to YogaNode
     widgetOptions: { borderColor: 'cyan' } → Saved for widget
     eventHandlers: { click: handleClick } → Saved for widget
     yogaNode: YogaNode
   }
      ↓
4. Yoga calculates layout
   YogaNode.calculateLayout()
   Result: { top: 0, left: 0, width: 100, height: 50 }
      ↓
5. Widget created/updated with positions and events
   Box {
     top: 0, left: 0, width: 100, height: 50,
     border: { type: 'line', fg: 6 }  // cyan converted to color code
   }
   box.on('click', handleClick)  // Event bound
      ↓
6. Rendered to terminal

Why Multiple Layers?

Each layer has a specific responsibility:

| Layer | Responsibility | | -------------- | ----------------------------------------------------- | | DOMNode | React reconciler - manages virtual DOM tree | | LayoutNode | Bridge between React props, layout engine, and events | | YogaNode | Pure flexbox calculations (no UI concerns) | | Widget | Terminal rendering (no layout concerns) |

This separation allows:

  • React to handle component lifecycle
  • Yoga to handle layout calculations
  • unblessed to handle terminal rendering
  • Each layer can be tested independently

Current Status

✅ Implemented:

  • React reconciler with full lifecycle support
  • Yoga flexbox layout engine integration
  • Components: Box, Text, Spacer, BigText, Button, Input, List
  • render() function with unmount/rerender support
  • Border styles and colors (per-side support)
  • Flexbox properties (gap, padding, margin, alignment, etc.)
  • Color conversion (string colors → terminal color codes)
  • Theme system with runtime switching (useTheme hook)
  • Design token architecture (primitives → semantic → components)
  • Two built-in themes (unblessedTheme, matrixTheme)
  • State styling (hover and focus effects)
  • Automatic theme fallback for all components
  • Theme color references ($primary, $semantic.success, etc.)
  • Event handling (onClick, onKeyPress, onSubmit, etc.)
  • Event cleanup on update/unmount
  • Content updates on state changes
  • Widget descriptor pattern for type-safe configuration
  • Composition via helper functions (borders, effects, focus, events)
  • 14+ tests passing

🚧 In Progress:

  • Hooks (useInput, useApp, useFocus)
  • Text wrapping and overflow handling
  • Comprehensive integration tests
  • Performance optimization

📋 Planned:

  • More component wrappers (ListTable, ProgressBar, Checkbox, RadioButton, etc.)
  • Animation support
  • Comprehensive documentation and examples

Contributing

See the main unblessed repository for contribution guidelines.

License

MIT © Vinicius De Antoni

Related