An open API service indexing awesome lists of open source software.

https://github.com/nick-skriabin/glyph

A React-based framework for building modern, composable terminal UIs.
https://github.com/nick-skriabin/glyph

cli flexbox react react-reconciler terminal tui typescript yoga

Last synced: 4 months ago
JSON representation

A React-based framework for building modern, composable terminal UIs.

Awesome Lists containing this project

README

          


Glyph

Glyph


React renderer for terminal UIs

Flexbox layout. Keyboard-driven. Zero compromises.


Quick Start
Components
Hooks
Styling
Examples


npm version
Tests
React 18+
Yoga Flexbox
TypeScript
MIT License

---

Build real terminal applications with React. Glyph provides a full component model with flexbox layout (powered by Yoga), focus management, keyboard input, and efficient diff-based rendering. Write TUIs the same way you write web apps.

| | | |
|---|---|---|
| ![Glyph](./screehshots/glyph-main.jpg) | ![Glyph List](./screehshots/glyph-list.jpg) |![Glyph Edit](./screehshots/glyph-edit.jpg) |

### Features

| | |
|---|---|
| **Flexbox Layout** | Full CSS-like flexbox via Yoga — rows, columns, wrapping, alignment, gaps, padding |
| **Rich Components** | Box, Text, Input, Button, Checkbox, Radio, Select, ScrollView, List, Menu, Progress, Spinner, Toasts, Dialogs, Portal, JumpNav |
| **Focus System** | Tab navigation, focus scopes, focus trapping for modals, JumpNav quick-jump hints |
| **Keyboard Input** | `useInput` hook, declarative `` component, vim-style bindings |
| **Smart Rendering** | Double-buffered framebuffer with character-level diffing — only changed cells are written |
| **True Colors** | Named colors, hex, RGB, 256-palette. Auto-contrast text on colored backgrounds |
| **Borders** | Single, double, rounded, and ASCII border styles |
| **TypeScript** | Full type coverage. Every prop, style, and hook is typed |

---

## Installation

```bash
# npm
npm install @nick-skriabin/glyph react

# pnpm
pnpm add @nick-skriabin/glyph react

# bun
bun add @nick-skriabin/glyph react
```

---

## Quick Start

```tsx
import React from "react";
import { render, Box, Text, Keybind, useApp } from "@nick-skriabin/glyph";

function App() {
const { exit } = useApp();

return (

Hello, Glyph!
exit()} />

);
}

render();
```

Run it:

```bash
npx tsx app.tsx
```

---

## Components

### ``

Flexbox container. The fundamental building block.

```tsx


Left


Right

```

### ``

Styled text content. Supports wrapping, alignment, bold, dim, italic, underline.

```tsx

Warning: something happened

```

### ``

Text input field with cursor and placeholder support.

```tsx

```

Supports `multiline` for multi-line editing, `autoFocus` for automatic focus on mount. The cursor is always visible when focused.

**Input types** for validation:

```tsx
// Text input (default) - accepts any character

// Number input - only accepts digits, decimal point, minus sign

```

**Input masking** with `onBeforeChange` for validation/formatting:

```tsx
import { createMask, masks } from "@nick-skriabin/glyph";

// Pre-built masks

// Custom masks: 9=digit, a=letter, *=alphanumeric
const licensePlate = createMask("aaa-9999");

```

Available masks: `usPhone`, `intlPhone`, `creditCard`, `dateUS`, `dateEU`, `dateISO`, `time`, `timeFull`, `ssn`, `zip`, `zipPlus4`, `ipv4`, `mac`.

### ``

Focusable button with press handling and visual feedback.

```tsx
console.log("clicked")}
style={{ border: "single", borderColor: "cyan", paddingX: 2 }}
focusedStyle={{ borderColor: "yellowBright", bold: true }}
>
Submit

```

Buttons participate in the focus system automatically. Press `Enter` or `Space` to activate.

### ``

Toggle checkbox with label support.

```tsx
const [agreed, setAgreed] = useState(false);

```

Focusable. Press `Enter` or `Space` to toggle. Supports custom `checkedChar` and `uncheckedChar` props.

### ``

Radio button group for single selection from multiple options.

```tsx
const [theme, setTheme] = useState("dark");

```

Focusable. Navigate with `Up`/`Down`/`Left`/`Right`/`Tab`/`Shift+Tab`, select with `Enter`/`Space`. Supports `direction` prop (`"column"` or `"row"`), custom `selectedChar` and `unselectedChar`.

### ``

Scrollable container with keyboard navigation and clipping.

```tsx

{items.map((item, i) => (

{item}

))}

```

**Keyboard:** `PageUp`/`PageDown`, `Ctrl+d`/`Ctrl+u` (half-page), `Ctrl+f`/`Ctrl+b` (full page).

Shows a scrollbar when content exceeds viewport (disable with `showScrollbar={false}`). Supports controlled mode with `scrollOffset` and `onScroll` props.

**Focus-aware scrolling:** ScrollView is focusable by default and responds to scroll keys when focused (or when it contains the focused element). This prevents multiple ScrollViews from scrolling simultaneously — only the one with focus responds.

Set `focusable={false}` if you want the ScrollView to only scroll when a child element has focus:

```tsx

{/* ScrollView scrolls only when Input is focused */}

```

### ``

Keyboard-navigable selection list with a render callback.

```tsx
handleSelect(items[index])}
disabledIndices={new Set([2, 5])}
renderItem={({ index, selected, focused }) => (


{selected ? "> " : " "}{items[index]}


)}
/>
```

Focusable. `Up`/`Down`/`j`/`k` to navigate, `G` to jump to bottom, `gg` to jump to top, `Enter` to select. Disabled indices are skipped.

### ``

Styled menu built on ``. Accepts structured items with labels, values, and disabled state.

```tsx
handleAction(value)}
highlightColor="yellow"
/>
```

### ``

Dropdown select with keyboard navigation and type-to-filter search.

```tsx
const [lang, setLang] = useState();

```

Focusable. `Enter`/`Space`/`Down` to open, `Up`/`Down` to navigate, `Enter` to confirm, `Escape` to close. Type characters to filter items when open. Disabled items are skipped.

Props: `items`, `value`, `onChange`, `placeholder`, `maxVisible`, `highlightColor`, `searchable`, `style`, `focusedStyle`, `dropdownStyle`, `disabled`.

### ``

Focus trapping for modals and overlays.

```tsx



OK

```

### ``

Renders children in a fullscreen absolute overlay. Useful for modals and dialogs.

```tsx



Modal content

```

### ``

Quick keyboard navigation to any focusable element. Press an activation key to show hint labels on all focusable elements, then type the hint to jump directly to that element. Similar to Vim's EasyMotion or browser extensions like Vimium.

```tsx
function App() {
return (





Submit


);
}
```

**How it works:**
1. Press `Ctrl+O` (or custom `activationKey`) to activate
2. Hint labels (a, s, d, f...) appear next to each focusable element
3. Type a hint to instantly focus that element
4. Press `Escape` to cancel

**Props:**

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `activationKey` | `string` | `"ctrl+o"` | Key to activate jump mode |
| `hintChars` | `string` | `"asdfghjkl..."` | Characters used for hints |
| `hintBg` | `Color` | `"yellow"` | Hint label background |
| `hintFg` | `Color` | `"black"` | Hint label text color |
| `hintStyle` | `Style` | `{}` | Additional hint label styling |
| `enabled` | `boolean` | `true` | Enable/disable JumpNav |

**Focus scope aware:** JumpNav automatically respects ``. When a modal with a focus trap is open, only elements inside that trap will show hints.

### ``

Declarative keyboard shortcut. Renders nothing.

```tsx

exit()} />
```

**Modifiers:** `ctrl`, `alt`, `shift`, `meta` (Cmd/Super). Combine with `+`: `"ctrl+shift+p"`, `"alt+return"`.

**Priority keybinds:** Use `priority` prop to run BEFORE focused input handlers. Useful for keybinds that should work even when an Input is focused:

```tsx

```

**Terminal configuration:** Some keybinds like `ctrl+return` require terminal support:

| Terminal | Configuration |
|----------|---------------|
| **Ghostty** | Add to `~/.config/ghostty/config`: `keybind = ctrl+enter=text:\x1b[13;5~` |
| **iTerm2** | Profiles → Keys → General → Enable "CSI u" mode |
| **Kitty/WezTerm** | Works out of the box |

`alt+return` works universally without configuration.

### ``

Determinate or indeterminate progress bar. Uses `useLayout` to measure actual width and renders block characters.

```tsx

```

Props: `value` (0..1), `indeterminate`, `width`, `label`, `showPercent`, `filled`/`empty` (characters).

### ``

Animated spinner with configurable frames. Cleans up timers on unmount.

```tsx

```

### `` + `useToast()`

Lightweight toast notifications rendered via Portal. Wrap your app in ``, then push toasts from anywhere with `useToast()`.

```tsx
function App() {
const toast = useToast();
return
toast({ message: "Saved!", variant: "success" })
} />;
}

render();
```

Variants: `"info"`, `"success"`, `"warning"`, `"error"`. Auto-dismiss after `durationMs` (default 3000).

### `` + `useDialog()`

Imperative `alert()` and `confirm()` dialogs, similar to browser APIs. Wrap your app in ``, then show dialogs from anywhere.

```tsx
function App() {
const { alert, confirm } = useDialog();

const handleDelete = async () => {
const ok = await confirm("Delete this item?", {
okText: "Delete",
cancelText: "Keep"
});
if (ok) {
// delete the item
}
};

const handleSave = async () => {
await saveData();
await alert("Saved successfully!");
};

return Delete;
}

render();
```

**Rich content** — pass React elements instead of strings:

```tsx
await alert(

✓ Success!
Your changes have been saved.
,
{ okText: "Got it!" }
);
```

**Keyboard:** Tab/Shift+Tab or arrows to switch buttons, Enter/Space to select, Escape to cancel.

**Chained dialogs** work naturally with async/await — each dialog waits for the previous to close.

### ``

Flexible space filler. Pushes siblings apart.

```tsx

Left

Right

```

---

## Hooks

### `useInput(handler)`

Listen for all keyboard events.

```tsx
useInput((key) => {
if (key.name === "escape") close();
if (key.ctrl && key.name === "s") save();
});
```

### `useFocus(nodeRef)`

Get focus state for a node.

```tsx
const ref = useRef(null);
const { focused, focus } = useFocus(ref);


{focused ? "* focused *" : "not focused"}

```

### `useFocusable(options)`

Make any element focusable with full keyboard support. Perfect for building custom interactive components.

```tsx
import { useFocusable, Box, Text } from "@nick-skriabin/glyph";

function CustomPicker({ items, onSelect }) {
const [selected, setSelected] = useState(0);

const { ref, isFocused } = useFocusable({
onKeyPress: (key) => {
if (key.name === "up") {
setSelected(s => Math.max(0, s - 1));
return true; // Consume the key
}
if (key.name === "down") {
setSelected(s => Math.min(items.length - 1, s + 1));
return true;
}
if (key.name === "return") {
onSelect(items[selected]);
return true;
}
return false; // Let other handlers process
},
onFocus: () => console.log("Picker focused"),
onBlur: () => console.log("Picker blurred"),
disabled: false, // Set to true to skip in tab order
});

return (

{items.map((item, i) => (

{i === selected ? "> " : " "}{item}

))}

);
}
```

Returns `{ ref, isFocused, focus, focusId }`. The `ref` must be attached to an element with `focusable` prop.

### `useLayout(nodeRef)`

Subscribe to a node's computed layout.

```tsx
const ref = useRef(null);
const layout = useLayout(ref);

// layout: { x, y, width, height, innerX, innerY, innerWidth, innerHeight }
```

### `useApp()`

Access app-level utilities.

```tsx
const { exit, columns, rows } = useApp();
```

---

## Styling

All components accept a `style` prop. Glyph uses Yoga for flexbox layout, so the model is familiar if you've used CSS flexbox or React Native.

### Layout

| Property | Type | Description |
|----------|------|-------------|
| `width`, `height` | `number \| "${n}%"` | Dimensions |
| `minWidth`, `minHeight` | `number` | Minimum dimensions |
| `maxWidth`, `maxHeight` | `number` | Maximum dimensions |
| `padding` | `number` | Padding on all sides |
| `paddingX`, `paddingY` | `number` | Horizontal / vertical padding |
| `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | `number` | Individual sides |
| `gap` | `number` | Gap between flex children |

### Flexbox

| Property | Type | Default |
|----------|------|---------|
| `flexDirection` | `"row" \| "column"` | `"column"` |
| `flexWrap` | `"nowrap" \| "wrap"` | `"nowrap"` |
| `justifyContent` | `"flex-start" \| "center" \| "flex-end" \| "space-between" \| "space-around"` | `"flex-start"` |
| `alignItems` | `"flex-start" \| "center" \| "flex-end" \| "stretch"` | `"stretch"` |
| `flexGrow` | `number` | `0` |
| `flexShrink` | `number` | `0` |

### Positioning

| Property | Type | Description |
|----------|------|-------------|
| `position` | `"relative" \| "absolute"` | Positioning mode |
| `top`, `right`, `bottom`, `left` | `number \| "${n}%"` | Offsets |
| `inset` | `number \| "${n}%"` | Shorthand for all four edges |
| `zIndex` | `number` | Stacking order |

### Visual

| Property | Type | Description |
|----------|------|-------------|
| `bg` | `Color` | Background color |
| `border` | `"none" \| "single" \| "double" \| "round" \| "ascii"` | Border style |
| `borderColor` | `Color` | Border color |
| `clip` | `boolean` | Clip overflowing children |

### Text

| Property | Type | Description |
|----------|------|-------------|
| `color` | `Color` | Text color |
| `bold` | `boolean` | Bold text |
| `dim` | `boolean` | Dimmed text |
| `italic` | `boolean` | Italic text |
| `underline` | `boolean` | Underlined text |
| `wrap` | `"wrap" \| "truncate" \| "ellipsis" \| "none"` | Text wrapping mode |
| `textAlign` | `"left" \| "center" \| "right"` | Text alignment |

### Colors

Colors can be specified as:

- **Named:** `"red"`, `"green"`, `"blueBright"`, `"whiteBright"`, etc.
- **Hex:** `"#ff0000"`, `"#1a1a2e"`
- **RGB:** `{ r: 255, g: 0, b: 0 }`
- **256-palette:** `0`–`255`

Text on colored backgrounds automatically picks black or white for contrast when no explicit color is set.

---

## `render(element, options?)`

Mount a React element to the terminal.

```tsx
const app = render(, {
stdout: process.stdout,
stdin: process.stdin,
debug: false,
useNativeCursor: true, // Use terminal's native cursor (default: true)
});

app.unmount(); // Tear down
app.exit(); // Unmount and exit process
```

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `stdout` | `NodeJS.WriteStream` | `process.stdout` | Output stream |
| `stdin` | `NodeJS.ReadStream` | `process.stdin` | Input stream |
| `debug` | `boolean` | `false` | Enable debug logging |
| `useNativeCursor` | `boolean` | `true` | Use terminal's native cursor instead of simulated one |

### Native Cursor

By default, Glyph uses the terminal's native cursor, which enables:

- **Cursor shaders** in terminals that support them (e.g., Ghostty)
- **Custom cursor shapes** (block, beam, underline) from terminal settings
- **Cursor animations** and blinking behavior

The native cursor is automatically shown when an input is focused and hidden otherwise.

To use the simulated cursor instead (inverted colors, no shader support):

```tsx
render(, { useNativeCursor: false });
```

---

## Examples

Interactive examples are included in the repo. Each demonstrates different components and patterns:

| Example | Description | Source |
|---------|-------------|--------|
| **basic-layout** | Flexbox layout fundamentals | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/basic-layout) |
| **modal-input** | Modal dialogs, input focus trapping | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/modal-input) |
| **scrollview-demo** | Scrollable content with keyboard navigation | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/scrollview-demo) |
| **list-demo** | Keyboard-navigable lists | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/list-demo) |
| **menu-demo** | Styled menus with icons | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/menu-demo) |
| **select-demo** | Dropdown select with search | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/select-demo) |
| **forms-demo** | Checkbox and Radio inputs | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/forms-demo) |
| **masked-input** | Input masks (phone, credit card, SSN) | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/masked-input) |
| **dialog-demo** | Alert and Confirm dialogs | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/dialog-demo) |
| **jump-nav** | Quick navigation with keyboard hints | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/jump-nav) |
| **showcase** | Progress bars, Spinners, Toasts | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/showcase) |
| **dashboard** | Full task manager (all components) | [View →](https://github.com/nick-skriabin/glyph/tree/main/examples/dashboard) |

### Running Examples Locally

```bash
# Clone and install
git clone https://github.com/nick-skriabin/glyph.git && cd glyph
bun install && bun run build

# Run any example
bun run --filter dev

# e.g.
bun run --filter dashboard dev
bun run --filter jump-nav dev
```

---

## Who Uses Glyph




Aion



Calendar & time management TUI

Using Glyph in your project? Let us know!

---

## Architecture

```
src/
├── reconciler/ React reconciler (host config + GlyphNode tree)
├── layout/ Yoga-based flexbox + text measurement
├── paint/ Framebuffer, character diffing, borders, colors
├── runtime/ Terminal raw mode, key parsing, OSC handling
├── components/ Box, Text, Input, Button, ScrollView, List, Menu, ...
├── hooks/ useInput, useFocus, useLayout, useApp
└── render.ts Entry point tying it all together
```

**Render pipeline:** React reconciler builds a GlyphNode tree → Yoga computes flexbox layout → painter rasterizes to a framebuffer → diff engine writes only changed cells to stdout.

---

## License

MIT

---


Built with React • Yoga • a lot of ANSI escape codes