github.com/skuznetsov/crystal_tui
0.1.0 / published Aug 2, 2026 / repository
Modern TUI framework for Crystal. Inspired by Textual (Python) and TurboVision (Borland). Features: reactive state, TCSS styling, overlay transparency.
Crystal TUI
A modern, Textual-inspired TUI (Terminal User Interface) framework for Crystal.
Features
- Rich Widget Library: 40+ widgets including Panel, Button, Input, DataTable, Tree, ListView, Log, and more
- CSS Styling: A TCSS subset with variables, selectors, and CSS hot reload
- Flexible Layout: Flexbox-like layout engine with fr units, percentages, and constraints
- DOM-like Event Routing: Capture and bubble hooks around child dispatch
- Reactive Properties: Automatic re-rendering on property changes
- Overlay System: Popups, dialogs, and menus that render above other widgets
Installation
Add to your shard.yml:
dependencies:
crystal_tui:
github: skuznetsov/crystal_tui
Then run:
shards install
Quick Start
require "crystal_tui"
class HelloWorld < Tui::App
def compose : Array(Tui::Widget)
[
Tui::Panel.new("Hello, World!", id: "main") do |panel|
panel.content = Tui::Label.new("Welcome to Crystal TUI!", id: "welcome")
end
] of Tui::Widget
end
end
HelloWorld.new.run
Widgets
Containers
Panel- Container with border and titleHBox/VBox- Horizontal/vertical layoutGrid- CSS grid-style layoutSplitContainer- Resizable split panesTabbedPanel- Tabbed contentCollapsible- Expandable sectionDialog- Modal dialog
Input
Button- Clickable buttonInput- Single-line text inputMaskedInput- Input with format mask (phone, date)TextEditor- Multi-line editorCheckbox- Toggle checkboxRadioGroup- Radio button groupComboBox- Dropdown selectSwitch- iOS-style toggleSlider- Range sliderCalendar- Date pickerColorPicker- Color selection (16/256 colors)TimePicker- Time selection (24h/12h)
Display
Label- Text displayHeader- App title bar with clockFooter- Key bindings barProgressBar- Progress indicatorLoadingIndicator- Animated spinnerToast- Popup notificationsRule- Visual dividerSparkline- Mini trend chartDigits- Large ASCII art numbersPlaceholder- Development placeholderPretty- Pretty-print data structures
Data
DataTable- Data grid with sortingTree- Hierarchical tree viewListView- Virtual scrolling listSelectionList- Multi-select list with checkboxesLog- Scrolling log viewer with levelsFilePanel- File browserTextViewer- Scrollable textMarkdownView- Markdown rendererLink- Clickable URL/text
Layout
IconSidebar- VSCode-style sidebarWindowManager- Draggable windows
CSS Styling
Crystal TUI uses TCSS (TUI CSS), a simplified CSS dialect:
/* Variables */
$primary: cyan;
$bg: rgb(30, 30, 40);
/* Type selector (Label consumes visual color properties) */
Label {
background: blue;
color: white;
}
/* ID selector */
#main-panel {
border: light white;
padding: 1;
}
/* Class selector (apply this class to a Label) */
.active {
background: $primary;
}
/* Pseudo-class (Panel consumes border properties) */
Panel:focus {
border: round yellow;
}
/* Descendant selector */
Panel Button {
margin: 1;
}
/* Child selector */
Panel > Label {
color: yellow;
}
CSS Properties
Layout:
width,height- Size (px, %, fr, auto)min-width,max-width,min-height,max-heightmargin,margin-top/right/bottom/leftpadding,padding-top/right/bottom/left
Visual:
background,color,text-*- Applied byLabel; other widgets retain constructor stylesborder- Applied byPanel
The base widget stores opacity, but rendering does not currently apply it.
State selectors are matched when the stylesheet is applied; hover assignment and automatic style recomputation after state changes are not enabled yet.
Hot Reload
Enable CSS hot reload for development:
class MyApp < Tui::App
def initialize
super
load_css("styles/app.tcss")
enable_css_hot_reload # Watch for changes
end
end
Event Handling
Crystal TUI routes events in a DOM-like capture/child/bubble order, familiar to web developers:
CAPTURE HOOK: App → Panel → Container (`on_capture`)
CHILD/TARGET: Button handles the event
BUBBLE HOOK: Button → Container → Panel → App (`on_event`)
Event Phases
- Capture hook -
on_captureruns before children and can intercept an event. - Child/target routing - Mouse events visit children; key and paste events go to the focused widget.
- Bubble hook -
on_eventruns after children have had a chance to handle the event.
The Event::Phase, target, and current_target accessors are reserved metadata: the current dispatcher does not populate them, so the phase predicates remain at their default state.
Handling Events
Override on_event for target/bubble phase handling (most common):
class MyWidget < Tui::Widget
def on_event(event : Tui::Event) : Bool
case event
when Tui::KeyEvent
if event.key.enter?
do_something
event.stop_propagation! # Stop bubble
return true
end
end
super
end
end
Override on_capture to intercept events BEFORE they reach children:
class MyApp < Tui::App
# Global hotkeys - intercept before any child can handle
def on_capture(event : Tui::Event) : Bool
if event.is_a?(Tui::KeyEvent)
if event.modifiers.ctrl? && event.char == 's'
save_document
event.stop_propagation! # Don't send to children
return true
elsif event.modifiers.ctrl? && event.char == 'q'
quit
event.stop_propagation!
return true
end
end
super
end
end
Event Control Methods
# Stop propagation to next widget (current widget's handlers still run)
event.stop_propagation!
# Stop propagation and set the immediate-stop flag
event.stop_immediate!
# Set the default-prevented flag (built-in widgets do not consume it yet)
event.prevent_default!
# Phase/target metadata is reserved for a future dispatcher update.
# `phase` remains `None`, and `target`/`current_target` remain nil today.
prevent_default! and stop_immediate! currently set event flags; no widget consumes the default-prevented flag, and immediate-handler semantics are reserved for a future dispatcher update.
Legacy Compatibility
Widgets that override handle_event directly continue to work with the legacy (depth-first) model. For new widgets, prefer using on_event and on_capture.
Examples
See the examples/ directory for complete examples:
hello.cr- Basic hello worldbuttons.cr- Button interactionstable.cr- DataTable usagepanels.cr- Panel layoutssplit_demo.cr- SplitContainernew_widgets_demo.cr- Header, Tree, Switch, Toastcss_hot_reload_demo.cr- CSS hot reloadvscode_layout.cr- IDE-style layout
Run an example:
crystal run examples/hello.cr
Development
# Run tests
crystal spec
# Build all examples
mkdir -p bin
for example in examples/*.cr; do
crystal build "$example" -o "bin/$(basename "$example" .cr)"
done
# Generate API docs (outputs to docs/)
crystal docs
# Then open docs/index.html in browser
License
MIT License - see LICENSE
Credits
Inspired by Textual for Python.