You are reading release documentation. Read the latest development docs.

The View DSL

Sycamore uses the view! macro as an ergonomic way to create complex user interfaces.

Dislike macros? Check out the builder API.

The view! macro always produces an expression of type View.

The simplest view is an empty view. This can be constructed like this:

view! {}

Equivalently, you can also use:

View::default()

Elements

Elements have a special terse syntax, as opposed to HTML. You only need to write the tag name a single time and you don’t need a bunch of angle brackets. Text nodes are just string literals.

These elements can also be nested and contain text nodes.

view! {
    // An empty div
    div {}
    // A div with a paragraph
    div {
        p { "Hello, world!" }
    }
    // Custom elements also work
    my-custom-element {}
}

Interpolation

Views can contain interpolated values. Anything that implements IntoView can be used. This includes strings, numbers, signals, functions that return these types, and other views.

When converting a function into a view, it automatically becomes a dynamic view. For child expressions, the view! macro automatically wraps complex expressions in a closure, so they update when their reactive dependencies change. This automatic wrapping applies to child expressions, not element attributes or properties; see Attributes below.

let value = 123;
let signal = create_signal(456);

view! {
    p {
        "Value: " (value)
    }
    p {
        // Equivalent of (move || signal.get() + 1)
        (signal.get() + 1)
    }
}

Interpolated strings

Text nodes can also interpolate values directly inside a string literal. These strings automatically become reactive: expressions are evaluated again whenever the signals they read change.

let name = create_signal("world");
let count = create_signal(0);

view! {
    p { "Hello, {name}! Count: {count}" }
}

Unlike format!, placeholders can contain arbitrary Rust expressions, including field access, method calls, indexing, and arithmetic:

view! {
    p { "Hello, {item.name}! Next: {count.get() + 1}" }
}

Rust’s standard format options are supported, including debug formatting, alignment, width, and precision. Width and precision can reference local usize variables using the usual $ syntax:

let width = 20;
let precision = 2;

view! {
    p { "Price: {price:.2}" }
    p { "{item.name:width$}" }
    p { "{price:.precision$} {unit}" }
}

Only inline expressions are supported: you cannot pass additional positional or named arguments after the string. Each placeholder must contain an expression; use {value} rather than {}.

Expressions are evaluated inside a reactive move closure, which owns the values it captures. Signals implement Display, so {name} formats the signal’s current value and tracks it automatically; .get() is only needed when using the value in a larger expression.

Use {{ and }} for literal braces in text nodes. Strings without placeholders remain static. This syntax applies only to text nodes, not string literals in attributes or component props. Rust string escapes are decoded before parsing interpolations; use a raw string when an expression itself contains escapes, for example r#"First line: {text.split('\n').next().unwrap()}"#.

Here is another example where we interpolate views.

let details = view! {
    div { "Details" }
};

let outer_view = view! {
    h1 { "Why is Rust the best language" }
    (details)
};

Control flow

The view! macro supports using if/else and match statements directly inside the DSL. Conditions and match expressions automatically track reactive dependencies, just like child expressions.

view! {
    if hidden.get() {
        p {
            "Can't see me!"
        }
    } else {
        p {
            "Peek a boo!"
        }
    }
}

Attributes

Attributes (including classes and ids) can also be specified. Static values are passed directly. To make an attribute or property reactive, pass a closure explicitly; unlike child expressions, the macro does not automatically wrap attribute or property expressions in a closure.

let disabled = create_signal(false);

view! {
    p(class="my-class", id="my-paragraph", aria-label="My paragraph", "custom_attribute"="foo")
    button(disabled=move || disabled.get()) {
       "My button"
    }
}

All attributes are type-checked so that, e.g. the disabled attribute only accepts values of type bool. For attributes that are not part of the HTML spec, you can wrap the attribute name in quotes to create a custom attribute.

Setting inner html

The special dangerously_set_inner_html attribute is used to set an HTML string as the child of an element. This should generally be avoided if possible because it is a possible security risk. Never pass user input to this attribute as that will create an XSS (Cross-Site Scripting) vulnerability.

view! {
    div(dangerously_set_inner_html="spanInner HTML!/span")

    // DO NOT DO THIS!!!
    div(dangerously_set_inner_html=user_input)
}

Interpolating strings directly do not have this problem since that will always result in a text node, not arbitrary HTML nodes.

Setting node refs

The special r#ref attribute is used to set a NodeRef to point at the element.

let node = create_node_ref();
view! {
    button(r#ref=node)
}

For more details, see Node Ref.

Properties

Properties are set using the prop:* directive.

view! {
    input(r#type="checkbox", prop:indeterminate=true)
}

There are some properties that do not have a matching attribute, such as indeterminate in HTML, and which must be set using the prop:* directive. Other properties such as value have unintuitive behavior when using the attribute version.

Events

Events are attached using the on_event attribute.

view! {
    button(on_click=|_| { /* do something interesting */ }) {
        "Click me"
    }
}

Optional attributes

Stringy attributes can also be optional. To make an attribute optional, simply pass in an Option! For example:

let attr = create_signal(None::String);
view! {
    div(data-attr=attr)
    // Will render div/div instead of div data-attr/div if attr is None.
}

Components

Calling components from the view! macro follows essentially the same syntax as for elements.

#[component]
fn Button(class: String) - View { ... }

view! {
    div {
        Button(class="my-button".to_string())
    }
}

Components can also accept children just like elements.