Skip to content

Native Widget Bridging

custom_widget replaces the old list / custom_list examples. Instead of only showing Item 1, it uses a deploy queue to explain when to keep using built-in components and when to drop down to a native ratatui widget.

custom widget demo

This recording is generated by docs/tapes/custom-widget.tape and runs examples/advanced/custom_widget.rs. Press j/k to move through the queue and Enter to submit the current item; the right panel updates with ListState.

cargo run --example custom_widget

Built-in Select, MultiSelect, TreeSelect, and VirtualList cover common selection scenarios. But the framework should not wrap every ratatui widget as a business component. For lower-level tables, charts, log panels, or custom painters, handwrite a Component and call ComponentDrawer directly during draw.

The outer layer is still a function component

Section titled “The outer layer is still a function component”

The outer CustomWidgetApp is still an ordinary #[component]. Keyboard events, exit, and state reads live there:

#[component]
fn CustomWidgetApp(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let list_state = hooks.use_state(|| {
        let mut state = ListState::default();
        state.select(Some(0));
        state
    });
    let mut status = hooks.use_state(|| "ready".to_string());
    let mut exit = hooks.use_exit();

    hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
        let Event::Key(key) = event else {
            return EventResult::Ignored;
        };

        match key.code {
            KeyCode::Char('j') | KeyCode::Down => move_selection(list_state, 1),
            KeyCode::Char('k') | KeyCode::Up => move_selection(list_state, -1),
            KeyCode::Enter => status.set(selection_message("submitted", list_state)),
            KeyCode::Char('q') | KeyCode::Char('Q') => exit(),
            _ => return EventResult::Ignored,
        }

        EventResult::Consumed
    });

    let queue_props = DeployQueueProps { state: list_state };
    element!(DeployQueue(..queue_props))
}

ListState lives in use_state, so writes wake the render loop. State<ListState> is a lightweight handle that can be passed by value to handwritten components and event closures. DeployQueue(..queue_props) is used because props contain a required explicit State<ListState> and do not go through default props construction.

The handwritten component has a narrow job: store the State<ListState> from props, synchronize the handle every frame in update, and render the native widget during draw.

#[derive(Props)]
struct DeployQueueProps {
    state: State<ListState>,
}

struct DeployQueue {
    state: State<ListState>,
}

impl Component for DeployQueue {
    type Props<'a> = DeployQueueProps;

    fn new(props: &Self::Props<'_>) -> Self {
        Self { state: props.state }
    }

    fn update(
        &mut self,
        props: &mut Self::Props<'_>,
        _hooks: Hooks,
        _updater: &mut ComponentUpdater,
    ) {
        self.state = props.state;
    }

    fn draw(&mut self, drawer: &mut ComponentDrawer<'_, '_>) {
        let list = List::new(items)
            .highlight_style(Style::new().black().on_cyan())
            .highlight_symbol("> ");

        drawer.render_stateful_widget(
            list,
            drawer.area,
            &mut self.state.write_no_update(),
        );
    }
}

write_no_update() is important: the draw stage is only rendering ratatui’s stateful widget into the buffer and should not trigger another reactive change. The actual selected item changes in the event handler through state.write().select(...).

A handwritten Component is closer to the runtime than a function component, so it can access ComponentUpdater and ComponentDrawer. These are not everyday business APIs, but they matter when you write a truly low-level component:

CapabilityStageUsage
Sync props into instance fieldsupdateCopy handles or config such as props.state and props.items into the component instance
Use hooksupdatePlain use_state can be used directly; hooks that need context, such as use_event_handler or use_context, need hooks.with_context_stack(...) first
Set layout fieldsupdateWhen props use #[with_layout_style], call updater.set_layout_style(props.layout_style())
Make a component transparent for layoutupdateCall updater.set_transparent_layout(true), usually done automatically by the #[component] macro
Manually update childrenupdateCall updater.update_children(elements, context), useful for components that organize their own subtree
Draw ratatui widgetsdrawUse drawer.render_widget or drawer.render_stateful_widget
Customize child regionscalc_children_areasThe default is flex layout; complex containers can override it, but the returned region count must equal the child count

The hooks value passed to a handwritten component is not context-aware by default. To register events or read Providers, upgrade it first:

fn update(
    &mut self,
    props: &mut Self::Props<'_>,
    hooks: Hooks,
    updater: &mut ComponentUpdater,
) {
    self.state = props.state;

    let mut hooks = hooks.with_context_stack(updater.component_context_stack());
    hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
        // ...
        EventResult::Ignored
    });
}

This step is needed only in handwritten Components. The #[component] macro handles it for function components, where you can normally call use_event_handler, use_exit, and use_context directly.

If you only need to read or modify context data, you can also go through ComponentUpdater directly:

if let Some(mut system) = updater.get_context_mut::<SystemContext>() {
    system.exit();
}

That API is lower-level and better suited to framework components. Business components should prefer hooks with semantic names, such as use_exit() instead of directly mutating SystemContext.

If you only need to place an existing widget in the element tree, use the adapter syntax in the macro:

element!(
    Border {
        widget(paragraph)
        stateful(list, list_state)
    }
)

If you need to construct a complex widget inside a component, split props, reuse drawing state, or plug into custom layout, handwrite Component. This boundary is clearer than wrapping everything as built-in components, and it keeps business components from polluting the framework API.

Next, see Custom Hook: stable state, side-effect, and subscription patterns can become your own business use_* functions.