Declarative syntax
Ratatui Kit’s everyday syntax is built on several macros:
element!: declare this frame’s UI tree.#[component]: turn a function into a persistentComponent.#[derive(Props)]: let props participate in runtime type erasure.#[with_layout_style]: inject layout fields into component props.routes!: declare a Router route table.
This page is a syntax reference. To see how state is preserved at runtime, read the component model. To see real if / for / match examples, read control flow syntax.
element!
Section titled “element!”The most common form is:
Parentheses contain props; braces contain children. If there are no props, omit the parentheses. If there are no children, omit the braces.
Field values go through .into(), so many props accept lighter call-site syntax than their field type:
element! fills omitted fields with Default::default(). That means custom props constructed through field syntax must implement Default.
If props cannot provide a reasonable default, construct the full props value in ordinary Rust code and pass it with ..props:
..rest must be the last item in the props list. When rest props are used, the macro does not append ..Default::default().
Control flow is first-class syntax
Section titled “Control flow is first-class syntax”Children can contain if, if let, for, and match directly:
Different branches may return different component types without manually calling .into_any(). This is declaration-layer control flow; do not call hooks inside those branches. Hook call order belongs to the component instance and must be stable every frame.
More complex dynamic expressions can still be embedded with { expr }:
Prefer first-class control flow. Use { expr } only when the expression naturally returns Option, Vec, an iterator, or an element.
key: is a reserved field and does not enter props. It only affects element identity:
The runtime reuses previous-frame component instances by ElementKey + TypeId. Use business IDs when list items can be inserted, deleted, or reordered. Use indices only for static arrays or small lists whose order never changes.
You cannot write key: on route components inside routes!. Route identity is determined by path, and the macro rejects this explicitly.
#[component]
Section titled “#[component]”Function components use #[component]:
The macro recognizes only these parameter names:
| Parameter name | Purpose |
|---|---|
hooks / _hooks | Receive Hooks, by value or by reference |
props / _props | Receive a props reference; the type must be a reference |
Other parameter names produce an error. This is not a style rule; the macro uses parameter names to decide what to pass to the generated implementation.
Function components are transparent layout components: they do not occupy independent layout nodes, and instead pass the returned root element to the runtime. Layout props should be written on the returned root element:
If you want a parent to write Panel(width: ...) and have Panel occupy its own layout slot, do not make it a no-props transparent wrapper. Give it props and explicitly forward layout fields to the internal root, or write Component by hand.
Custom props must implement Props:
If Default cannot be derived, write a semantically neutral default:
For fields with no reasonable default, such as State<ListState>, prefer constructing the complete props at the call site and passing Component(..props). Do not fabricate an invalid state handle just to satisfy Default.
Optional fields should use Option<T>. Because field values are converted with .into(), all three forms below work:
Ratatui 0.30’s Block<'static> can be used directly as a props field, for example block: Option<Block<'static>>. Do not reintroduce Send/Sync wrappers; the current runtime is single-threaded for rendering, and the framework no longer forces props to be Send + Sync.
Hand-written components without props use NoProps:
Function components usually do not need to mention NoProps; #[component] generates the corresponding props type. Use NoProps explicitly only for hand-written Components, adapters, or test helpers.
Internally, the runtime stores different component props in AnyProps. This is a type-erased container whose correctness depends on reconciliation already having matched component type and props type. Application code should not construct or downcast AnyProps directly; expose concrete props types and let element! and the runtime handle erasure.
Callback props
Section titled “Callback props”Built-in component callbacks such as on_change, on_select, and on_close use Handler<'a, T, V = ()>. Callers usually do not need to write Handler::from because element! calls .into() for field values:
Custom components that expose callbacks should prefer the same type:
Handler::default() is an empty implementation, and the return type V must implement Default. Inside a component, use take() before moving a callback into an event closure. Use is_default() when you need to distinguish whether the user passed a callback.
with_layout_style
Section titled “with_layout_style”If a component should support width, height, margin, offset, gap, flex_direction, and justify_content, add #[with_layout_style] to its props:
You can also inject only part of the layout fields:
#[with_layout_style] only works on named-field structs. It generates a layout_style() method, which hand-written components should call during update:
widget / stateful adapter
Section titled “widget / stateful adapter”element! can bridge Ratatui widgets directly:
widget(expr) accepts exactly one expression. stateful(widget, state) accepts exactly two arguments: the widget and the state. The old $ / #(...) adapter syntax has been removed.
Adapters are good for simple bridges:
- Native stateless widgets such as
Paragraph,Line, andBlock. - Native stateful widgets such as
ListandTablethat need Ratatui state.
If you need to construct complex widgets inside a component, hold props, register events, or override layout, write Component by hand. See Native widget bridge for the complete example.
routes!
Section titled “routes!”routes! reuses the props-head syntax from element!:
Parentheses () are route component props; braces {} are child routes. Props passed through routes! enter a 'static route table and cannot borrow stack temporaries.
Do not put dynamic data into route props:
- Dynamic path segments come from
use_params(). - Lightweight context for one navigation comes from
push_with_state/try_use_route_state::<T>(). - Cross-page business state belongs in Atom or an outer Provider.
See Routing for more route boundaries.