Base props

Every node accepts these, whatever component it names. A component page lists what that component adds; this is the set underneath all of them.

import { Panel } from '@textui/widgets';

<Panel
  title="Services"
  padding={1}
  gap={1}
  border="single"
  focusable
  label="service list"
  onKey={(event) => event.name === 'r' && void app.execute('services.refresh')}
/>

Four things are worth knowing before the tables.

Style arrives three ways

The same declaration can be written inline, as an object, or as a stateful map, and they compose rather than compete:

<>
  <box gap={1} padding={1} border="single" />
  <box style={{ gap: 1, padding: 1 }} />
  <box style={{ base: { fg: 'muted' }, focus: { fg: 'accent', bold: true } }} />
</>

Inline keys are the convenience form for the common case. Reach for style when a value is stateful - base / focus / disabled - or when it is computed and passing twenty props would be worse than passing one object.

label and role are not decoration

They are what the test harness queries by, and what future accessibility work will read. A node with an interactive role is focusable without saying so:

<box role="button" label="Restart the server" onClick={() => {}} />

That is a working button - focusable, in the tab order, named - without importing Button. Button exists because it also draws a ring, states a tone, and inverts when focused.

onKey normally needs focus

A handler runs while its node is focused. global opts out of that, for a node that wants the keys its children decline:

<box global onKey={(event) => event.name === 'escape'} />

Use it for a wrapper - a dropdown taking left and right while the menu inside keeps up and down. Everywhere else, focus is what decides who gets a key, and global throws that away.

breakpoints are per node, not per screen

A node renders compact below the first width and minimal below the second. The widths are its own, not the terminal’s, so a panel in a narrow column degrades while the same component in the main pane does not.

The tables

BaseProps

Props every node accepts. Style arrives three ways on purpose: the full style object for anything stateful, a merged list for composition, and the individual style keys inline as convenience props - <box gap={1} border="single"> rather than <box style={{ gap: 1, border: 'single' }}> for the common case.

Prop Type  
id string  
key string \| number  
style StyleInput  
role SemanticRole Semantic metadata. Drives the test harness, and future a11y work.
label string  
description string  
disabled boolean  
selected boolean  
focused boolean The states this node is in, for style overlays and for a theme. focused is a tri-state on purpose. Left out, the runtime asks the focus manager - which is right for a control and wrong for a row: a list row does not hold the keyboard, the list does. A row that has to be told so, or the distinction between “this is the current row” and “this is the current row and you can type at it” cannot be drawn at all.
styleAs string Which of the component’s boxes a theme styles. A composite component draws plain box nodes, so without this its entry in a theme’s components map is a key nothing reads - a list row is a box, and components.List.selected is what a theme author would reach for. The name has to be stated by the component that owns the box, which is the only place that knows what the row is part of. Omitted, the node is styled under its own host name, so components.box and a variant keep working exactly as they did.
focusable boolean Participates in tab order. Implied by an interactive role.
focusScope string The focus scope this node belongs to.
autoFocus boolean  
global boolean onKey runs whether or not this node is focused. For a node that wraps something else and wants the keys that thing declines - a dropdown panel taking left and right while the menu inside it keeps up and down. Without this a handler only runs while focused, which is what focus means.
onKey (event: KeyEvent) => boolean \| void  
onFocus () => void  
onBlur () => void  
onMouse (event: MouseEvent) => boolean \| void Every mouse action on this node, innermost first. Returning true stops it going any further - and on a down, claims the rest of the gesture: the drags and the up that follow come here whatever they are over, until the button comes back up. Dispatch is otherwise a hit test, so without that a drag would stop at the edge of the node it started in, which is where a drag starts being worth having.
onClick Action \| ((event: MouseEvent) => void) The left button going down - a third of a gesture. onMouse for the rest.
onHover (hovering: boolean) => void The pointer entered or left this node. Called once each way, not per cell. Hover is inherited the way it is in a browser: a row is hovered while the pointer is over the label inside it, because the label is what a hit test finds. A style with a hover overlay needs nothing else - this is for the cases where something other than a colour has to happen.
link string OSC 8 link target, where the terminal supports hyperlinks.
breakpoints { compact?: number; minimal?: number } Below this width the node renders compact; below that, minimal.

BoxProps

Prop Type  
children unknown  
title string Header text drawn into the top border. Needs a border to land on.
titleAlign 'left' \| 'center' \| 'right'  
rightTitle string A second label on the top border, hard against the right. For the short thing that belongs beside a heading rather than under it - a count, a shortcut, a state. It takes its space first and title gets what is left, so the two never collide and the title is the one that truncates.
footer string Footer text drawn into the bottom border.
footerAlign 'left' \| 'center' \| 'right'  
scrollTop number Scroll offset in cells, when overflow is ‘scroll’.
scrollLeft number  

TextProps

Prop Type  
children unknown  
content string The string to draw. children is accepted as a shorthand.
truncate 'end' \| 'start' \| 'middle' \| false Where to cut when the text does not fit.
ellipsis string  
match string Text to pick out wherever it appears, case-insensitively. For search: the caller passes what it is looking for and the rows are coloured where they hold it. It is applied after wrapping and truncation, to the text as drawn - so nothing about how a paragraph is broken into lines has to change to mark a hit in it, and a match split across a wrap is simply not on either row to colour.
matchFg StyleColor The match’s colours. Accent on its own foreground by default.
matchBg StyleColor  

CanvasProps

Prop Type  
draw (surface: PaintSurface, ctx: RenderContext) => void Paint directly. The escape hatch charts and gauges use; everything else should compose box and text so the layout engine can reason about it.
intrinsic { width?: number; height?: number } Intrinsic size when the style does not fix one.

SpacerProps

| Prop | Type | | | — | — | — | | size | number | Cells to take. Unset means “take whatever is left”, the same as flex: 1. |


Back to top

MIT licensed. Pre-1.0 - the surface is still moving.