Skip to content
wirelessEye edited this page Jul 2, 2026 · 6 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
}

Use #[props(raw)] to keep a non-nested field as its declared type instead of wrapping it in PropValue<T>.

#[props]
pub struct WorkerProps {
    #[props(raw)]
    runtime: RuntimeHandle,
}

// `runtime` is a `RuntimeHandle`, not `PropValue<RuntimeHandle>`.
let runtime = &props.runtime;

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,
}

Use container-level #[props(default)] to generate a Default implementation for the props type. Every field must either have a default field attribute or be an Option<T>.

#[props(default)]
pub struct PanelProps {
    #[props(default)]
    title: String,
    subtitle: Option<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)] 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.

Nested Props

Use #[props(nested)] to embed another props type without wrapping that field in PropValue<T>. The macro also generates a hidden field_builder method on the generated builder for recursive builder syntax. Use #[props(nested(args...))] when the nested props type has positional start fields.

#[props]
pub struct ViewProps {
    #[props(start)]
    x: i32,
    #[props(start)]
    y: f32,
    #[props(default)]
    margin: f32,
}

#[props]
pub struct ButtonProps {
    #[props(nested(x: i32, y: f32))]
    view_props: ViewProps,

    #[props(default)]
    title: String,
}

let props = build_props!(ButtonProps(
    .view_props(
        10,
        20.0,
        .margin = 3.0,
    ),
    .title = "Click".to_string(),
));

Nested fields cannot be #[props(start)] fields or grouped fields. Raw fields cannot also be nested 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.

For #[props(nested)] fields, replace = with parentheses to recursively build the nested props. Positional values inside the parentheses are passed to the generated nested builder helper:

let props = build_props!(ButtonProps(
    .view_props(
        10,
        20.0,
        .margin = 3.0,
    ),
));

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