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.