vue-termui
Build terminal interfaces with Vue 3 and the public vue-termui API. This file covers layout and common components. Load a reference only when the task needs it:
- Markdown or images: specialized components
- Additional implementation patterns: cookbook
- Focus, events, refs, renderer constraints, lifecycle, Vite, or 3D: advanced
- Core component implementation or a new wrapper: component development
Essential rules
- In apps, import components, composables, and Vue APIs from
vue-termui. Do not import from vue or @opentui/core.
- Use PascalCase public components. Treat
tui-* tags as internal renderer hosts.
- Use
Box for layout and Text for text. Do not rely on CSS, HTML, DOM APIs, or browser behavior.
- Keep text nodes inside
Text. Its children must resolve directly to strings or
text nodes: do not nest components or put conditional <template> fragments
inside it. Compute conditional lines as strings, or use styled-text helpers
for inline runs.
- Prefer percentage or flex sizing inside bordered containers. Terminal dimensions are measured in cells.
- Give
ScrollBox, images, editors, and other content a useful constraint through width, height, or flex props.
- Use the existing component source, co-located spec, and a similar playground page as the current API reference.
- Keep changes focused. Run the narrowest relevant test, then
pnpm test:types and pnpm lint.
Choose a component
Import every item from vue-termui. Native layout and rendering options pass through unless the wrapper documents otherwise.
| Need |
Component |
Important contract |
| Layout, border, spacing |
Box |
Flexbox container with padding, margin, borders, positioning, overflow, and mouse handlers. |
| Scrolling |
ScrollBox |
Scrollable container. Constrain its viewport. |
| Styled text |
Text |
Slot content, colors, wrapping, and text attributes. Defaults to flexShrink: 0. |
| Line breaks |
Newline |
count defaults to 1. |
| Single-line editing |
Input |
Synchronized v-model; emits input, change, and enter. |
| Multi-line editing |
Textarea |
v-model seeds once; Meta or Cmd plus Enter emits submit. |
| Vertical choice |
Select |
v-model is the highlighted index; select commits an option. |
| Horizontal choice |
TabSelect |
v-model is the highlighted index; movement and commit use separate events. |
| Progress |
ProgressBar |
Horizontal bar with value, optional max, width, character, and colors. |
Box
Box is the flexbox layout container. These value types repeat below:
Dimension: a cell count such as 20, 'auto', or a percentage such as '50%'
Color: a named/hex string or an OpenTUI RGBA value (ColorInput)
- Gap/padding: a cell count or percentage; margin and position also accept
'auto'
Size and flex
| Props |
Values |
What they do |
width, height |
Dimension |
Set the box size. Borders and padding use space inside it. |
minWidth, minHeight, maxWidth, maxHeight |
Dimension |
Limit the size produced by content or flex layout. |
flexDirection |
'row', 'row-reverse', 'column', 'column-reverse' |
Set the main axis and child order. |
flexGrow |
number |
Claim a share of unused space. 0 does not grow. |
flexShrink |
number |
Set how much the box may shrink. Explicit numeric width/height defaults this to 0; otherwise it defaults to 1. |
flexBasis |
number, 'auto' |
Set the initial main-axis size before growing or shrinking. |
flexWrap |
'no-wrap', 'wrap', 'wrap-reverse' |
Keep children on one line or wrap them across lines. |
justifyContent |
'flex-start', 'center', 'flex-end', 'space-between', 'space-around', 'space-evenly' |
Position children on the main axis. |
alignItems |
'auto', 'flex-start', 'center', 'flex-end', 'stretch', 'baseline', 'space-between', 'space-around', 'space-evenly' |
Align all children on the cross axis. |
alignSelf |
same values as alignItems |
Override the parent's cross-axis alignment for this box. |
gap, rowGap, columnGap |
number, percentage |
Add space between children on both axes or one axis. |
Numeric sizes are terminal cells. Percentage dimensions and flex growth adapt to available space. A fixed width can overflow a parent's interior because borders and padding consume cells; use width="100%" or flexGrow inside framed layouts.
<Box :width="60" :height="12" flexDirection="column" :gap="1" border :padding="1">
<Box flexDirection="row" alignItems="center" :columnGap="2">
<Text>Fixed</Text>
<Box :flexGrow="1"><Text>Uses remaining width</Text></Box>
</Box>
<Box :flexGrow="1" justifyContent="center" alignItems="center">
<Text>Centered in remaining space</Text>
</Box>
</Box>
Spacing
| Props |
Values |
What they do |
padding |
number, percentage |
Set inner spacing on all sides. |
paddingX, paddingY |
number, percentage |
Set left/right or top/bottom inner spacing. |
paddingTop, paddingRight, paddingBottom, paddingLeft |
number, percentage |
Set one inner side. Side props override broader shorthands. |
margin |
Dimension |
Set outer spacing on all sides. 'auto' absorbs available space. |
marginX, marginY |
Dimension |
Set left/right or top/bottom outer spacing. |
marginTop, marginRight, marginBottom, marginLeft |
Dimension |
Set one outer side. Side props override broader shorthands. |
Position and overflow
| Props |
Values |
What they do |
position |
'static', 'relative', 'absolute' |
Use normal flow, offset normal flow, or remove the box from flow. |
top, right, bottom, left |
Dimension |
Offset a relative/absolute box from the named edge. |
zIndex |
number |
Set sibling paint order; larger values paint later. |
overflow |
'visible', 'hidden', 'scroll' |
Paint overflow, clip it, or give Yoga scroll overflow semantics. Use ScrollBox for an interactive viewport. |
Border and fill
| Props |
Values |
What they do |
backgroundColor |
Color |
Set the cell background. |
shouldFill |
boolean |
Fill the box background; defaults to true. Disable for border-only drawing. |
border |
boolean, array of 'top', 'right', 'bottom', 'left' |
Draw all border sides or only selected sides. |
borderStyle |
'single', 'double', 'rounded', 'heavy' |
Select border characters. |
customBorderChars |
{ topLeft, topRight, bottomLeft, bottomRight, horizontal, vertical, topT, bottomT, leftT, rightT, cross } |
Replace every border character. |
borderColor |
Color |
Set the normal border color. |
focusedBorderColor |
Color |
Set the border color while this box or a descendant has focus. The box need not be focusable. |
title, bottomTitle |
string |
Put text in the top or bottom border. |
titleColor |
Color |
Set title text color. |
titleAlignment, bottomTitleAlignment |
'left', 'center', 'right' |
Align the matching border title. |
<Box
:border="['top', 'bottom']"
borderStyle="rounded"
borderColor="#666"
focusedBorderColor="#42b883"
title=" Results "
titleAlignment="center"
backgroundColor="#111"
:paddingX="2"
>
<Text>Content</Text>
</Box>
Visibility, focus, and identity
| Props |
Values |
What they do |
id |
string |
Identify the native renderable for lookup and debugging. |
visible |
boolean |
Show the box. false removes it from flex layout and blurs it. |
opacity |
number |
Set subtree opacity, clamped to 0–1. |
focusable |
boolean |
Allow focus; defaults to false for Box. |
autofocus |
boolean |
Focus on mount when focusable. The first mounted autofocus target wins. |
onFocus, onBlur, onDestroyed |
functions with no arguments |
Handle wrapper lifecycle events; template equivalents are @focus, @blur, @destroyed. |
onSizeChange |
function with no arguments |
Run after layout changes the rendered width or height. |
Input handlers
All handlers receive the native event. Keyboard and paste handlers only run while the box is focused, so also set focusable.
| Props |
What they handle |
onMouse |
Every mouse event. |
onMouseDown, onMouseUp, onMouseMove |
Button and pointer movement. |
onMouseDrag, onMouseDragEnd, onMouseDrop |
Drag lifecycle. |
onMouseOver, onMouseOut, onMouseScroll |
Hover boundaries and wheel/scroll input. |
onKeyDown |
Focused keyboard input. |
onPaste |
Focused paste input. |
Low-level rendering
These native OpenTUI props are only relevant to custom rendering or animation:
| Props |
Values |
What they do |
buffered |
boolean |
Render through a box-sized offscreen buffer. Set it when the box is created. |
live |
boolean |
Keep the renderer's live loop active for continuous updates. |
renderBefore, renderAfter |
(buffer, deltaTime) => void |
Draw immediately before or after the box. |
enableLayout |
boolean |
Present in OpenTUI's public option type but currently has no runtime effect. Do not use it. |
Put layout siblings in a Box. Give ScrollBox a width, height, or flex constraint so it has a viewport. Its scrollX and scrollY options are constructor-only; change a :key to remount when they must change.
Text
Put every visible string and interpolation inside Text. Text accepts slot content plus fg, bg, wrap, bold, dim, italic, underline, strikethrough, inverse, and blink.
Do not nest Text. For inline colors or attributes, pass content built with t, fg, bg, bold, italic, and the other styled-text helpers exported by vue-termui.
Text defaults to flexShrink: 0. A flex-shrunk wrapped text renderable can paint over following rows, so override this only with a tested layout. Use Newline for one or more explicit line breaks.
Common component contracts
Input is controlled through v-model. input fires for each edit, change after a committed change, and enter on Return.
Textarea uses its model as the initial buffer, then owns its cursor, undo history, and content. Changing the model later does not replace the buffer. Change a :key to reset it. Meta or Cmd plus Enter emits submit.
Select and TabSelect models contain an option index, not its value. Select options require name and can include description and value; it emits select(option, index) on commit. TabSelect options require name and description; it emits changed on movement and selected on commit.
ProgressBar requires value; max defaults to 1, numeric width to 25, and
char to █. Its width is a cell count, not a general layout Dimension, so
percentages such as width="100%" are invalid. Derive a responsive number
from useTerminalSize. Use color and trackColor for its filled and empty
cells.
Input, Textarea, Select, TabSelect, and ScrollBox are focusable by default. Use autofocus for the initial target and focusable="false" to remove an element from managed navigation. The first mounted autofocus target wins. A focused ScrollBox supports arrows, hjkl, PageUp/PageDown, Home/End, and the mouse wheel.
Basic composition
<script setup lang="ts">
import { Box, Input, ref, Text } from 'vue-termui'
const name = ref('')
</script>
<template>
<Box flexDirection="column" :gap="1" borderStyle="rounded" :padding="1">
<Text bold fg="#42b883">Profile</Text>
<Input v-model="name" width="100%" placeholder="Name" autofocus />
<Text dim>{{ name || 'Waiting for input' }}</Text>
</Box>
</template>
Runtime
Creating an OpenTUI renderer requires Node.js 26.3 or newer with --experimental-ffi. Renderer tests use:
NODE_OPTIONS='--experimental-ffi --disable-warning=ExperimentalWarning' pnpm exec vitest run <spec>
The playground dev command already supplies the required flag:
pnpm --filter playground dev
For standalone apps and examples, typechecking and building cannot catch every
native render-tree constraint. After those checks, launch the built app briefly
under the real renderer, exercise at least one interaction, and quit cleanly:
node --experimental-ffi --disable-warning=ExperimentalWarning dist/main.js
1---2name: vue-termui3description: Build, refactor, debug, or extend Vue 3 terminal apps with vue-termui and OpenTUI. Use for components, layout, text, forms, selection, scrolling, focus, keyboard or mouse input, images, Markdown, renderer behavior, playground pages, and native component wrappers.4---56# vue-termui78Build terminal interfaces with Vue 3 and the public `vue-termui` API. This file covers layout and common components. Load a reference only when the task needs it:910- Markdown or images: [specialized components](references/components.md)11- Additional implementation patterns: [cookbook](references/cookbook.md)12- Focus, events, refs, renderer constraints, lifecycle, Vite, or 3D: [advanced](references/advanced.md)13- Core component implementation or a new wrapper: [component development](references/component-development.md)1415## Essential rules1617- In apps, import components, composables, and Vue APIs from `vue-termui`. Do not import from `vue` or `@opentui/core`.18- Use PascalCase public components. Treat `tui-*` tags as internal renderer hosts.19- Use `Box` for layout and `Text` for text. Do not rely on CSS, HTML, DOM APIs, or browser behavior.20- Keep text nodes inside `Text`. Its children must resolve directly to strings or21 text nodes: do not nest components or put conditional `<template>` fragments22 inside it. Compute conditional lines as strings, or use styled-text helpers23 for inline runs.24- Prefer percentage or flex sizing inside bordered containers. Terminal dimensions are measured in cells.25- Give `ScrollBox`, images, editors, and other content a useful constraint through width, height, or flex props.26- Use the existing component source, co-located spec, and a similar playground page as the current API reference.27- Keep changes focused. Run the narrowest relevant test, then `pnpm test:types` and `pnpm lint`.2829## Choose a component3031Import every item from `vue-termui`. Native layout and rendering options pass through unless the wrapper documents otherwise.3233| Need | Component | Important contract |34| ----------------------- | ------------- | ------------------------------------------------------------------------------------------- |35| Layout, border, spacing | `Box` | Flexbox container with padding, margin, borders, positioning, overflow, and mouse handlers. |36| Scrolling | `ScrollBox` | Scrollable container. Constrain its viewport. |37| Styled text | `Text` | Slot content, colors, wrapping, and text attributes. Defaults to `flexShrink: 0`. |38| Line breaks | `Newline` | `count` defaults to 1. |39| Single-line editing | `Input` | Synchronized `v-model`; emits `input`, `change`, and `enter`. |40| Multi-line editing | `Textarea` | `v-model` seeds once; Meta or Cmd plus Enter emits `submit`. |41| Vertical choice | `Select` | `v-model` is the highlighted index; `select` commits an option. |42| Horizontal choice | `TabSelect` | `v-model` is the highlighted index; movement and commit use separate events. |43| Progress | `ProgressBar` | Horizontal bar with `value`, optional `max`, width, character, and colors. |4445## Box4647`Box` is the flexbox layout container. These value types repeat below:4849- `Dimension`: a cell count such as `20`, `'auto'`, or a percentage such as `'50%'`50- `Color`: a named/hex string or an OpenTUI `RGBA` value (`ColorInput`)51- Gap/padding: a cell count or percentage; margin and position also accept `'auto'`5253### Size and flex5455| Props | Values | What they do |56| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |57| `width`, `height` | `Dimension` | Set the box size. Borders and padding use space inside it. |58| `minWidth`, `minHeight`, `maxWidth`, `maxHeight` | `Dimension` | Limit the size produced by content or flex layout. |59| `flexDirection` | `'row'`, `'row-reverse'`, `'column'`, `'column-reverse'` | Set the main axis and child order. |60| `flexGrow` | `number` | Claim a share of unused space. `0` does not grow. |61| `flexShrink` | `number` | Set how much the box may shrink. Explicit numeric width/height defaults this to `0`; otherwise it defaults to `1`. |62| `flexBasis` | `number`, `'auto'` | Set the initial main-axis size before growing or shrinking. |63| `flexWrap` | `'no-wrap'`, `'wrap'`, `'wrap-reverse'` | Keep children on one line or wrap them across lines. |64| `justifyContent` | `'flex-start'`, `'center'`, `'flex-end'`, `'space-between'`, `'space-around'`, `'space-evenly'` | Position children on the main axis. |65| `alignItems` | `'auto'`, `'flex-start'`, `'center'`, `'flex-end'`, `'stretch'`, `'baseline'`, `'space-between'`, `'space-around'`, `'space-evenly'` | Align all children on the cross axis. |66| `alignSelf` | same values as `alignItems` | Override the parent's cross-axis alignment for this box. |67| `gap`, `rowGap`, `columnGap` | `number`, percentage | Add space between children on both axes or one axis. |6869Numeric sizes are terminal cells. Percentage dimensions and flex growth adapt to available space. A fixed width can overflow a parent's interior because borders and padding consume cells; use `width="100%"` or `flexGrow` inside framed layouts.7071```vue-html72<Box :width="60" :height="12" flexDirection="column" :gap="1" border :padding="1">73 <Box flexDirection="row" alignItems="center" :columnGap="2">74 <Text>Fixed</Text>75 <Box :flexGrow="1"><Text>Uses remaining width</Text></Box>76 </Box>77 <Box :flexGrow="1" justifyContent="center" alignItems="center">78 <Text>Centered in remaining space</Text>79 </Box>80</Box>81```8283### Spacing8485| Props | Values | What they do |86| ------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------- |87| `padding` | `number`, percentage | Set inner spacing on all sides. |88| `paddingX`, `paddingY` | `number`, percentage | Set left/right or top/bottom inner spacing. |89| `paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft` | `number`, percentage | Set one inner side. Side props override broader shorthands. |90| `margin` | `Dimension` | Set outer spacing on all sides. `'auto'` absorbs available space. |91| `marginX`, `marginY` | `Dimension` | Set left/right or top/bottom outer spacing. |92| `marginTop`, `marginRight`, `marginBottom`, `marginLeft` | `Dimension` | Set one outer side. Side props override broader shorthands. |9394### Position and overflow9596| Props | Values | What they do |97| -------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------- |98| `position` | `'static'`, `'relative'`, `'absolute'` | Use normal flow, offset normal flow, or remove the box from flow. |99| `top`, `right`, `bottom`, `left` | `Dimension` | Offset a relative/absolute box from the named edge. |100| `zIndex` | `number` | Set sibling paint order; larger values paint later. |101| `overflow` | `'visible'`, `'hidden'`, `'scroll'` | Paint overflow, clip it, or give Yoga scroll overflow semantics. Use `ScrollBox` for an interactive viewport. |102103### Border and fill104105| Props | Values | What they do |106| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |107| `backgroundColor` | `Color` | Set the cell background. |108| `shouldFill` | `boolean` | Fill the box background; defaults to `true`. Disable for border-only drawing. |109| `border` | `boolean`, array of `'top'`, `'right'`, `'bottom'`, `'left'` | Draw all border sides or only selected sides. |110| `borderStyle` | `'single'`, `'double'`, `'rounded'`, `'heavy'` | Select border characters. |111| `customBorderChars` | `{ topLeft, topRight, bottomLeft, bottomRight, horizontal, vertical, topT, bottomT, leftT, rightT, cross }` | Replace every border character. |112| `borderColor` | `Color` | Set the normal border color. |113| `focusedBorderColor` | `Color` | Set the border color while this box or a descendant has focus. The box need not be focusable. |114| `title`, `bottomTitle` | `string` | Put text in the top or bottom border. |115| `titleColor` | `Color` | Set title text color. |116| `titleAlignment`, `bottomTitleAlignment` | `'left'`, `'center'`, `'right'` | Align the matching border title. |117118```vue-html119<Box120 :border="['top', 'bottom']"121 borderStyle="rounded"122 borderColor="#666"123 focusedBorderColor="#42b883"124 title=" Results "125 titleAlignment="center"126 backgroundColor="#111"127 :paddingX="2"128>129 <Text>Content</Text>130</Box>131```132133### Visibility, focus, and identity134135| Props | Values | What they do |136| ---------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------ |137| `id` | `string` | Identify the native renderable for lookup and debugging. |138| `visible` | `boolean` | Show the box. `false` removes it from flex layout and blurs it. |139| `opacity` | `number` | Set subtree opacity, clamped to `0`–`1`. |140| `focusable` | `boolean` | Allow focus; defaults to `false` for `Box`. |141| `autofocus` | `boolean` | Focus on mount when focusable. The first mounted autofocus target wins. |142| `onFocus`, `onBlur`, `onDestroyed` | functions with no arguments | Handle wrapper lifecycle events; template equivalents are `@focus`, `@blur`, `@destroyed`. |143| `onSizeChange` | function with no arguments | Run after layout changes the rendered width or height. |144145### Input handlers146147All handlers receive the native event. Keyboard and paste handlers only run while the box is focused, so also set `focusable`.148149| Props | What they handle |150| ---------------------------------------------- | ---------------------------------------- |151| `onMouse` | Every mouse event. |152| `onMouseDown`, `onMouseUp`, `onMouseMove` | Button and pointer movement. |153| `onMouseDrag`, `onMouseDragEnd`, `onMouseDrop` | Drag lifecycle. |154| `onMouseOver`, `onMouseOut`, `onMouseScroll` | Hover boundaries and wheel/scroll input. |155| `onKeyDown` | Focused keyboard input. |156| `onPaste` | Focused paste input. |157158### Low-level rendering159160These native OpenTUI props are only relevant to custom rendering or animation:161162| Props | Values | What they do |163| ----------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------- |164| `buffered` | `boolean` | Render through a box-sized offscreen buffer. Set it when the box is created. |165| `live` | `boolean` | Keep the renderer's live loop active for continuous updates. |166| `renderBefore`, `renderAfter` | `(buffer, deltaTime) => void` | Draw immediately before or after the box. |167| `enableLayout` | `boolean` | Present in OpenTUI's public option type but currently has no runtime effect. Do not use it. |168169Put layout siblings in a `Box`. Give `ScrollBox` a width, height, or flex constraint so it has a viewport. Its `scrollX` and `scrollY` options are constructor-only; change a `:key` to remount when they must change.170171## Text172173Put every visible string and interpolation inside `Text`. `Text` accepts slot content plus `fg`, `bg`, `wrap`, `bold`, `dim`, `italic`, `underline`, `strikethrough`, `inverse`, and `blink`.174175Do not nest `Text`. For inline colors or attributes, pass `content` built with `t`, `fg`, `bg`, `bold`, `italic`, and the other styled-text helpers exported by `vue-termui`.176177`Text` defaults to `flexShrink: 0`. A flex-shrunk wrapped text renderable can paint over following rows, so override this only with a tested layout. Use `Newline` for one or more explicit line breaks.178179## Common component contracts180181- `Input` is controlled through `v-model`. `input` fires for each edit, `change` after a committed change, and `enter` on Return.182- `Textarea` uses its model as the initial buffer, then owns its cursor, undo history, and content. Changing the model later does not replace the buffer. Change a `:key` to reset it. Meta or Cmd plus Enter emits `submit`.183- `Select` and `TabSelect` models contain an option index, not its value. `Select` options require `name` and can include `description` and `value`; it emits `select(option, index)` on commit. `TabSelect` options require `name` and `description`; it emits `changed` on movement and `selected` on commit.184- `ProgressBar` requires `value`; `max` defaults to 1, numeric `width` to 25, and185 `char` to `█`. Its width is a cell count, not a general layout `Dimension`, so186 percentages such as `width="100%"` are invalid. Derive a responsive number187 from `useTerminalSize`. Use `color` and `trackColor` for its filled and empty188 cells.189- `Input`, `Textarea`, `Select`, `TabSelect`, and `ScrollBox` are focusable by default. Use `autofocus` for the initial target and `focusable="false"` to remove an element from managed navigation. The first mounted autofocus target wins. A focused `ScrollBox` supports arrows, `hjkl`, PageUp/PageDown, Home/End, and the mouse wheel.190191## Basic composition192193```vue194<script setup lang="ts">195import { Box, Input, ref, Text } from 'vue-termui'196197const name = ref('')198</script>199200<template>201 <Box flexDirection="column" :gap="1" borderStyle="rounded" :padding="1">202 <Text bold fg="#42b883">Profile</Text>203 <Input v-model="name" width="100%" placeholder="Name" autofocus />204 <Text dim>{{ name || 'Waiting for input' }}</Text>205 </Box>206</template>207```208209## Runtime210211Creating an OpenTUI renderer requires Node.js 26.3 or newer with `--experimental-ffi`. Renderer tests use:212213```bash214NODE_OPTIONS='--experimental-ffi --disable-warning=ExperimentalWarning' pnpm exec vitest run <spec>215```216217The playground dev command already supplies the required flag:218219```bash220pnpm --filter playground dev221```222223For standalone apps and examples, typechecking and building cannot catch every224native render-tree constraint. After those checks, launch the built app briefly225under the real renderer, exercise at least one interaction, and quit cleanly:226227```bash228node --experimental-ffi --disable-warning=ExperimentalWarning dist/main.js229```