zgui

Views

The view! grammar in full — calls, attribute lists, children blocks, and what a view is as a value.

A view describes a piece of the interface. You write one with the view! macro, which reads a small grammar of nested calls and blocks. This page covers that grammar completely and does not use signals; Reactivity in views adds those.

The shape

use zgui::prelude::*;

view! {
    column(class = "card") {
        label(class = "card__title") {"Storage"}
        row(class = "card__row") {
            text {"Used"}
            spacer()
            text {"41 GB"}
        }
    }
}

Every node is a head, followed by an attribute list, a children block, or both.

column(class = "card"){ … }
column
The head: an element or component name.
(class = "card")
A comma-separated attribute list in parentheses.
{ … }
The children block in braces.
The parts of a view node

Either part may be left out. Both may not.

view! {
    spacer()             // attributes only, and there are none
    label {"Total"}      // children only
    label() {"Total"}    // the same thing, written out
    label {}             // no attributes, no children
}

A bare name is not a node. spacer on its own is a compile error that tells you the three things it could have meant: spacer() for a childless element, {spacer} for the value of a variable, or "spacer" for the text.

The three kinds of node

The first token decides which one you wrote, and nothing after it can change the answer.

You writeIt isExample
a string literaltext"Save"
a braced expressiona value converted into a view{label}
an identifiera call: an element or a componentrow(...)
view! {
    row {
        "a literal"        // text
        {name}             // an expression
        column()           // a call
    }
}

Text

A string literal is a text child. It needs no element around it.

label {"Save"}

Braced expressions

A braced child is exactly one expression. Its value is converted into a view.

let title = String::from("Storage");

view! {
    label {{title}}                        // a String
    text {{format!("{n} left")}}           // any expression
    box {{maybe_footer}}                   // an Option<impl IntoView> renders nothing when None
}

The doubled braces are not a mistake. The outer pair is the children block; the inner pair is the braced expression.

A bare expression is not a child. column { item.label.clone() } does not compile: item is read as a name beginning a call. Wrap it: column { {item.label.clone()} }. The same applies to a macro call — write {format!("{n} left")}, not format!("{n} left").

Calls

An identifier begins a call. Whether it is an element or a component is decided by the case of the name:

  • lower case — an element: row, text, control. Resolved by the macro from the vocabulary.
  • upper camel case — a component: Counter, Card. An ordinary Rust function you wrote.

A head can also be a path, which is how a vocabulary other than the built-in one is reached:

html::div()          // an element from a foreign vocabulary
ui::Button()         // a component from another module

Hyphens are allowed in an element name and become underscores: overlay-root() calls overlay_root(). Rust keywords work as heads: box() is the element named box.

Children are juxtaposed

Children are written one after another with no separator. No commas, no closing tags.

row {
    "a"
    {b}
    column()
}

Nesting is unlimited, and every level reads the same way.

A { after an attribute list is always that call's children, whatever whitespace stands between them. A macro cannot see line breaks. So this adopts the block as children:

vector(class = "axes")
{"x"}                    // a child of `vector`, not a sibling

Write an empty block to keep them apart:

vector(class = "axes") {}
{"x"}                    // now a sibling

Attributes

Attributes go inside the parentheses, separated by commas, each written name = value. A trailing comma is allowed.

control(
    class = "button",
    tabindex = Focus::Sequential,
    a11y:label = "Save",
    on:click = save,
)

A name written with no value is shorthand for name = name:

let hidden = true;
box(hidden)             // the same as box(hidden = hidden)

Values are ordinary Rust expressions

An attribute value is any expression Rust admits. Commas inside it belong to it.

row(state:open = count.get() > 0)
row(prop:mask = bits >> 2)
row(n = value as Wrapping<u8>)
row(x = if a > b { p } else { q })
row(f = |x| -> Vec<u8> { x })
row(on:click = move |ev: &mut EventCx<'_, Click>| f(ev))

// A comma inside a closure is the closure's.
For(key = |a: &Todo, b: &Todo| a.id > b.id, each = items) { "x" }

Braces around a value are optional and mean nothing: class = {class} and class = class produce the same code.

One expression needs its braces: a struct literal, because a { after a value would otherwise open a children block. Write at = {Point { x, y }}. The error message tells you this if you forget.

Namespaced attributes

A prefix before a colon selects what kind of attribute it is. Ten namespaces exist; this page names them, and Attributes documents each one.

row(
    class = "row",                    // the class list
    class:active = on,                // one class, toggled
    style:gap = "1rem",               // one inline style property
    var:--brand = "red",              // a CSS custom property
    attr:data-testid = "row",         // an attribute selectors can match
    prop:value = v,                   // a typed property on the element
    state:disabled,                   // a built-in interaction state
    custom_state:picked = p,          // a state of your own
    on:click = handler,               // a listener
    a11y:label = "Save",              // accessibility
    node_ref = handle,                // a handle to the created node
)

A custom property keeps the dashes it is declared with: var:--brand, not var:brand.

Spreading a bundle of attributes

{..expr} replays a prepared bundle of attributes at the position it is written. Attributes before it apply first, attributes after it apply last, so a later one wins.

Button(class:mine = true, {..attrs}, attr:data-x = "1")

A spread goes in the attribute list. Written among the children it is a compile error that says so.

A view is a value

view! is an expression. It produces a value that implements IntoView, and you can bind it, pass it, and return it like anything else.

let header = view! { label(class = "title") {"Storage"} };

view! {
    column {
        {header}
        text {"41 GB"}
    }
}

Several roots make a fragment

A view! with more than one root node produces a tuple. A view! with none produces ().

let two = view! {
    label {"a"}
    label {"b"}
};

Both render as siblings wherever they are placed. There is no wrapper element and no extra node in the document.

What implements IntoView

You rarely name it, but it explains what may sit in a braced child.

TypeWhat it renders
&'static str, String, Rc<str>one text node, written again only when the string changes
bool, char, and the integer and float primitivestheir to_string()
()nothing, and contributes no node
tuples, up to 26 membersa fragment: the members in order, each keeping its own type
Option<V>None renders nothing but keeps the place; Some to Some rebuilds in place
Vec<V>the items, matched by position — see the warning below
Result<V, E>the Err arm renders nothing and hands the error to the nearest error boundary — a wrapper that shows a fallback in its place. See Error handling
a closure returning a viewa reactive hole: a part of the view the framework runs again when what it read changes. See Reactivity in views
an element or component callthat element or component
AnyViewwhatever was erased into it

Vec<V> is positional, not keyed: it rebuilds the shared prefix in place, builds the extra items and unmounts the surplus. That is right for a fixed set of children and wrong for a collection with identity. Use for for anything a user can insert into, remove from or reorder.

Where a view goes

At the top of an application, run takes a closure that builds the root view:

fn main() -> Result<(), zgui::Error> {
    app()
        .with_stylesheet(SHEET)
        .run(|| view! { column(class = "app") { text {"Hello"} } })
}

Inside a component, the view is what the function returns:

#[component]
fn Header() -> impl IntoView {
    view! { label(class = "title") {"Storage"} }
}

And a component is called like any other node:

view! {
    column {
        Header()
        text {"41 GB"}
    }
}

What the macro refuses

The grammar is small, so the errors are specific. These are worth recognising:

You wroteThe message
spacer`spacer` is a name, not a node — with the three things it could have been
<row/>`<` cannot begin a node — the tag spelling was replaced by this grammar
column { 42 }expected a node — brace it: {42}
column { item.label() }`item` is a name, not a node
row() (class = "a")`(` cannot begin a node — one attribute list, and it comes first
row(class = "a" hidden)attributes are separated by commas
row(at = Point { x, y })a struct literal — brace the value
Card({attrs})names the spread spelling, {..attrs}
while() {}has no meaning in a viewfor and if are the control flow a view has

The grammar, formally

For reference:

view      = node* ;
node      = text | block | call | flow ;
text      = STRING_LITERAL ;
block     = "{" expression "}" ;
call      = head ( attrs children? | children ) ;
head      = name | path ;
name      = IDENT_ANY segment* ;
segment   = "-" ( IDENT_ANY | INT_LITERAL ) ;
path      = IDENT ( "::" IDENT )+ ;
attrs     = "(" ( attr ( "," attr )* ","? )? ")" ;
children  = "{" node* "}" ;

flow is for and if, which Control flow covers.

cargo fmt does not format the inside of a view! block. It cannot: the contents are not Rust expressions. Indent by hand, and keep one node per line.

Next

The names you can call are a fixed vocabulary of sixteen.

On this page