crystal_tui
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: Textual-compatible CSS (TCSS) for styling with variables, selectors, and hot reload
- Flexible Layout: Flexbox-like layout engine with fr units, percentages, and constraints
- DOM-like Event Model: Capture/bubble phases familiar to web developers
- 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:
tui:
github: skuznetsov/crystal_tui
Then run:
shards install
Quick Start
require "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", text: "Welcome to Crystal TUI!")
end
]
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 */
Button {
background: blue;
color: white;
}
/* ID selector */
#main-panel {
border: light white;
padding: 1;
}
/* Class selector */
.active {
background: $primary;
}
/* Pseudo-class */
Button:focus {
background: white;
color: black;
}
/* 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- Background colorcolor- Text colorborder- Border style and color
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 uses a DOM-like event model with capture and bubble phases, familiar to web developers:
CAPTURE (down): App → Panel → Container → Button
TARGET: Button handles the event
BUBBLE (up): Button → Container → Panel → App
Event Phases
- Capture Phase - Event travels from root DOWN to target. Allows parent widgets to intercept events before they reach children.
- Target Phase - Event is at the target widget (deepest widget for mouse, focused widget for keyboard).
- Bubble Phase - Event travels from target UP to root. Allows parent widgets to react after children.
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
false
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) : Nil
if event.is_a?(Tui::KeyEvent)
if event.modifiers.ctrl? && event.char == 's'
save_document
event.stop_propagation! # Don't send to children
elsif event.modifiers.ctrl? && event.char == 'q'
quit
event.stop_propagation!
end
end
end
end
Event Control Methods
# Stop propagation to next widget (current widget's handlers still run)
event.stop_propagation!
# Stop immediately (no more handlers at all)
event.stop_immediate!
# Prevent default action (widget-specific behavior)
event.prevent_default!
# Check event phase
event.capturing? # In capture phase?
event.at_target? # At target widget?
event.bubbling? # In bubble phase?
# Get target/current widget
event.target # Original target widget
event.current_target # Widget currently handling event
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_demo.cr- IDE-style layout
Run an example:
crystal run examples/hello.cr
Development
# Run tests
crystal spec
# Build all examples
crystal build examples/*.cr -o bin/
# 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.
crystal_tui
- 7
- 0
- 0
- 1
- 1
- 2 days ago
- January 7, 2026
MIT License
Wed, 28 Jan 2026 06:33:37 GMT