Skip to content
wirelessEye edited this page Jun 29, 2026 · 8 revisions

Props

The #[props] macro turns a named-field struct into a Nestix prop container and typed builder.

#[props(debug)]
#[derive(Debug)]
pub struct ButtonProps {
    #[props(default)]
    disabled: bool,
    on_click: Option<Shared<dyn Fn()>>,
}

Every normal field is transformed into PropValue<T>. Component code reads the current value with .get().

if props.disabled.get() {
    // disabled
}

Required Fields

Fields are required unless they are Option<T> or marked with #[props(default)] or #[props(start)].

#[props]
pub struct TextProps {
    text: String,
}

layout! {
    Text(.text = "Hello".to_string())
}

The generated builder tracks required fields at the type level.

Optional Fields

Option<T> fields default to None automatically.

#[props]
pub struct LinkProps {
    href: String,
    target: Option<String>,
}

Callers may omit .target.

Default Fields

Use #[props(default)] to default through Default::default().

#[props]
pub struct InputProps {
    #[props(default)]
    value: String,
}

Use #[props(default = expr)] for a custom default.

#[props]
pub struct BadgeProps {
    #[props(default = "neutral".to_string())]
    tone: String,
}

Start Fields

Use #[props(start)] for positional arguments in layout! and build_props!.

#[props(debug)]
#[derive(Debug)]
pub struct TextProps {
    #[props(start)]
    text: String,
}

layout! {
    Text("Hello")
}

Start fields are passed to the generated builder constructor before named properties.

Debug Props

#[props(debug)] makes the generated Props::debug_fmt implementation delegate to the struct's Debug implementation.

#[props(debug)]
#[derive(Debug)]
pub struct PanelProps {
    #[props(default)]
    children: Layout,
}

Without debug, type-erased props format as Props(..).

Generic Bounds

Use #[props(bounds(...))] to provide bounds for generated implementations.

#[props(bounds(I: IntoIterator + 'static, K: 'static))]
pub struct ForProps<I: IntoIterator, K> {
    data: I,
    key: Shared<dyn Fn(&I::Item) -> K>,
}

Grouped Builder Methods

Use #[props(group(name => [field_a, field_b]))] to generate a builder method that sets several normal prop fields to the same value.

#[props(
    group(margin => [margin_left, margin_right, margin_top, margin_bottom]),
    group(margin_vertical => [margin_top, margin_bottom]),
)]
pub struct ViewProps {
    #[props(default)]
    margin_left: Dimension,
    #[props(default)]
    margin_right: Dimension,
    #[props(default)]
    margin_top: Dimension,
    #[props(default)]
    margin_bottom: Dimension,
}

layout! {
    View(
        .margin = Dimension::Points(8.0),
        .width = Dimension::Points(200.0),
    )
}

Grouped fields must all have the same type. They cannot be #[props(start)] or #[props(extends(...))] fields. The generated method follows the same typed builder rules as field setters, so a grouped setter can set fields that are unset or defaulted.

When the prop struct is extensible, group methods are also exposed through generated builder extension traits, so extended prop structs can use the same group syntax.

Extensible Props

#[props(extensible(TraitName, WrapperTraitName))] generates a trait exposing the prop struct and its fields, plus a builder wrapper trait used for prop composition. Other prop structs can include an extension field with #[props(extends(TraitName, WrapperTraitName))].

This is useful for component libraries that want prop inheritance or shared prop groups.

#[props(extensible(ViewPropsExt, ViewPropsWrapper))]
pub struct ViewProps {
    #[props(default)]
    class: String,
}

#[props]
pub struct ButtonProps {
    #[props(extends(ViewPropsExt, ViewPropsWrapper))]
    view: ViewProps,
    on_click: Option<Shared<dyn Fn()>>,
}

Extension fields are not wrapped in PropValue<T> and cannot also be start or default fields.

build_props!

build_props! drives the generated builder. This is what layout! uses for component prop syntax.

let props = build_props!(ButtonProps(
    .disabled = false,
    .on_click = callback!(|| log::info!("clicked")),
));

Positional arguments fill start fields:

let props = build_props!(TextProps("Hello"));

Named values are converted with prop_value!, so plain values, signals, and existing PropValue<T> values are accepted.

prop_value!

prop_value! converts:

  • plain values to PropValue::from_plain;
  • signals to PropValue::from_signal;
  • existing PropValue<T> values unchanged.
let plain = prop_value!("Hello".to_string());
let reactive = prop_value!(computed!([count] || count.get().to_string()));

Clone this wiki locally