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 List you arrow through, a TextInput you type into, a Form you submit.
  • Static renderables just draw — a Badge, a Sparkline, a StatusBar. 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


This site uses Just the Docs, a documentation theme for Jekyll.