Components
Components are reusable UI objects — the widgets you compose into views. They come in two kinds:
- Interactive components sit in a controller’s focus ring, handle key and mouse events, and report back through return values — a
Listyou arrow through, aTextInputyou type into, aFormyou submit. - Static renderables just draw — a
Badge, aSparkline, aStatusBar. Some animate when you drive them from a timer (Spinner,Progressbar) — for physics-based motion, see Animation.
Every component inherits from Charming::Component, so assigns passed to new become reader methods and render can use the view DSL. Render any of them from a template or view with render_component:
<%= render_component Charming::Components::List.new(
items: ["Alpha", "Beta", "Gamma"],
selected_index: 0,
theme: theme
) %>
One rule underlies everything: components have no lifecycle. Declare each interactive component with the controller’s slot DSL, where it lives for the screen’s lifetime. Anything that must survive navigation (a selected index, a filter query, text in a field) lives in controller state or session and gets passed back in through the factory. Each component page shows this idiom.
Declared slots
Declare an interactive component once, at the class level:
class SearchController < ApplicationController
slot :query { Charming::Components::TextInput.new(placeholder: "Search…") }
slot :results { Charming::Components::List.new(items: []) }
on_submit :query, :run_search
end
The slot declaration does three jobs. It memoizes the component for the screen’s lifetime, so interaction state (selection, scroll, cursor) survives across events. It defines a private reader with the slot’s name, so actions and views can call results. And it feeds the default focus ring: with no explicit focus_ring, Tab cycles the declared slots in declaration order. An explicit focus_ring still wins, and may name layout panes that have no component (like :sidebar).
The factory block runs against the controller instance, so it can read params, state, and theme.
Refresh a memoized component’s data in the action, before render. The component keeps its interaction state; the data setter reclamps the selection:
def show
results.items = Entry.recent_first.to_a
render :show, results: results
end
List#items=, Table#rows=, Tree#nodes=, TabBar#tabs=, Autocomplete#suggestions=, and MultiSelectList#items= all work this way.
Two exceptions rebuild per dispatch instead of memoizing: forms (declare a plain method that calls form(:name)) and per-model modal components built from the record under review. Both capture values that change between dispatches, so they stay methods.
The old private-method convention still resolves but warns once per slot. Declare the slot to silence it; the convention is removed at 1.0. A focusable layout pane that nothing declares — no slot, no focus_ring entry, no same-named method — raises Charming::UnknownSlot in development and test.
Pickers & navigation
| Component | What it does |
|---|---|
| List | Selectable list with keyboard navigation, mouse support, and fuzzy filtering. |
| MultiSelectList | List with [x] checkboxes — Space toggles, Enter submits the checked set. |
| Table | Unicode data table with a scrolling window, sortable columns, and row selection. |
| Tree | Collapsible hierarchy — expand and collapse branches, select leaves. |
| Filepicker | Directory browser — descend into folders, pick a file. |
| TabBar | Horizontal tabs — arrow between them, Enter or click selects. |
| Breadcrumbs | Home › Projects › Current trail with the last item highlighted. |
| Paginator | Page tracker rendering ○ ● ○ dots or 2/3; slices the current page for you. |
| Viewport | Scrollable window over tall content, with wrapping and horizontal scroll. |
Text & input
| Component | What it does |
|---|---|
| TextInput | Single-line text field — masking for passwords, shell-style history, paste support. |
| TextArea | Multiline editor — Enter inserts a newline, wide-character aware. |
| Autocomplete | Text input with a live-filtered suggestion dropdown. |
| Form | Multi-field form with inputs, selects, confirms, validation, and submit/cancel. |
| CommandPalette | Fuzzy-search command picker, plus the modal chrome that frames it. |
Overlays & messaging
| Component | What it does |
|---|---|
| Modal | Centered overlay dialog with title, help text, and an optionally scrollable body. |
| Toast | Auto-dismissing notification box with info/success/warn/error accents. |
| HelpOverlay | Keyboard cheat-sheet modal, buildable straight from a controller’s key bindings. |
| StatusBar | One-row bar with left/center/right segments and key-hint pairs. |
| Badge | Inline styled pill for statuses, counts, and versions. |
| EmptyState | “Nothing here yet” placeholder with loading and error variants. |
| ErrorScreen | The panel the runtime renders for unhandled exceptions. |
Progress & time
| Component | What it does |
|---|---|
| Spinner | Animated frame-cycling indicator with named presets — :dots, :moon, :meter, … |
| ActivityIndicator | Gradient activity bar with label and ellipsis animation. |
| Progressbar | Text progress bar with optional gradient fill and percent tracking. |
| Timer | Countdown clock (mm:ss) — tick it from a controller timer. |
| Stopwatch | Count-up clock — start, stop, reset; accumulates only while running. |
Data & media
| Component | What it does |
|---|---|
| Chart | Line charts (braille subpixels) and bar charts in a fixed-size box. |
| Sparkline | One-line ▁▂▄▇ bar graph, one cell per value. |
| Markdown | CommonMark/GFM renderer with syntax highlighting and clickable links. |
| Image | Inline terminal images on Kitty/Ghostty, with a text fallback everywhere else. |
| Audio | One-line playback-status indicator for an audio player. |
How components talk to controllers
Interactive components return Charming::Components::Result objects from handle_key / handle_mouse, and the runtime dispatches them to controller actions declared for the focus slot:
| Return value | Controller action |
|---|---|
Result.handled | — (event consumed) |
Result.selected(object) | declared with on_select :slot, :action — the action receives the object |
Result.submitted(value) | declared with on_submit :slot, :action — the action receives the value |
Result.cancelled | declared with on_cancel :slot, :action — no arguments |
nil | — (event falls through) |
The legacy forms (:handled, [:selected, object], [:submitted, value], :cancelled) still work in components you wrote before this release — the dispatch pipeline normalizes them to Result with no deprecation. New code returns Result.
The full protocol — key handling, text capture, paste, mouse events, theming — lives in Build Your Own, along with charming generate component for scaffolding your own.
Table of contents
- List
- MultiSelectList
- Table
- Tree
- Filepicker
- TabBar
- Breadcrumbs
- Paginator
- Viewport
- TextInput
- TextArea
- Autocomplete
- Form
- CommandPalette
- Modal
- Toast
- HelpOverlay
- StatusBar
- Badge
- EmptyState
- ErrorScreen
- Spinner
- ActivityIndicator
- Progressbar
- Timer
- Stopwatch
- Chart
- Sparkline
- Markdown
- Image
- Audio
- Build Your Own