Skip to content

feat: all-you-can-inline html! overhaul#4118

Merged
Madoshakalaka merged 6 commits intomasterfrom
feat/html-match-and-let-in-for
Apr 9, 2026
Merged

feat: all-you-can-inline html! overhaul#4118
Madoshakalaka merged 6 commits intomasterfrom
feat/html-match-and-let-in-for

Conversation

@Madoshakalaka
Copy link
Copy Markdown
Member

@Madoshakalaka Madoshakalaka commented Apr 6, 2026

Description

Various macro improvements that help users write concise Yew code.

Automatic root-level fragments:

The html! macro now accepts multiple root nodes directly:

html! {
    <h1>{"Title"}</h1>
    <p>{"Paragraph"}</p>
}

Streamlined match arms

Previously, match required a block wrapper and nested html! calls per arm:

html! {
    <div>{
        match status {
            Status::Loading => html! { <Spinner /> },
            Status::Ready(data) => html! { <DataView data={data} /> },
        }
    }</div>
}

Now match works directly, following the same pattern as the existing if/else support, with let binding allowed:

html! {
    match status {
        Status::Ready(data) => {
            let data_pretty = format!("My data: {data}");
            let class = if data.important { "highlight" } else { "normal" };
            <DataView data={data_pretty} class={class} />
        }
        _ => <Spinner/>
    }
}

As shown above, match arms with a single element don't require braces:

html! {
    match status {
        Status::Loading => <Spinner/>,
        Status::Error(e) => <p class="error">{e}</p>,
        Status::Ready(data) => {
            <DataView data={data} />
        }
    }
}

Note, even the commas are omittable but due to users' familarity with Rust match statements, I have preserved the commas in code examples for now.

let bindings in for bodies:

Previously, let bindings inside for loops required a nested block with an inner html! call:

html! {
    for item in items {
        {{ let processed = transform(&item); html! { <div>{processed.name}</div> } }}
    }
}

Now let bindings can appear directly before html children:

html! {
    for item in items {
        let processed = transform(&item);
        let class = if processed.active { "active" } else { "inactive" };
        <div class={class}>{processed.name}</div>
    }
}

let bindings must appear before any html children (not interleaved). This avoids parsing ambiguity since let is not a valid start for any HtmlTree variant. The bindings are emitted inside the Iterator::for_each closure, scoped to each iteration.

If statements let-binding and bare return node support

This now just works:

html! {
    if condition {
        let label = format!("count: {count}");
        <span>{label}</span>
    } else {
        "nothing"
    }
}

Note:

Both worked on master:

html! { "foo" }
html! { <div>{"foo"}</div>  }

however, only the first of the two below worked:

html!{
  if foo {
    <div>{"foo"}</div>
  }
}
html!{
  if foo {
    "foo"
  }
}

I consider this a bug, which is fixed now.

Multi-children arms, multi-children for-loop bodies, and multi-children if-else bodies

Example:

html!{
  if foo {
    <span>{"123"}</span>
    <span>{"234"}</span>
  }
}

we now protest against old patterns

Patterns like this are now hated against with a suggestion to remove the <></>:

html!{
  if foo {
    <>
      <span>{"123"}</span>
      <span>{"234"}</span>
    </>
  }
}

Keyed fragments (<key="...">) are unaffected.

Patterns like this are hated against with a suggestion to remove the inner html!:

html!{
  match foo {
    0 => {
      html!{
        // ...
      }
    }
    // ...
  }
}

On stable, these are hard errors. On nightly, these are warnings.

I must note, these deprecation lints are vibe-coding friendly. Agents can work with a clear feedback and correct macro errors in closed loops. (note errors reported by proc_macro_error::emit_error! will batch together, unlike compiler_error!'s early stop behavior. This will help accelerate adoption of the new syntax across user applications.

That said, these lints actually increase code complexity and slow down compilation.
We can remove them X versions later, when users have caught up with the newer syntax. Then, we can remove old pattern parsing code too.

Observation

The new macro brings a 22% reduction to applicable code, as it brings real code size reduction to our examples (+443 / −565 (net −122 lines) across 38 files under examples/).

Prior Art

Focusing on some features we haven't adopted yet:

@kirillsemyonkin's yew-alt-html has a super dry syntax that allows closing tag omission and brace omission:

html!{
  <h1 style=italic_style>"Hello " name "!"</>
}

closing tag omission is neat because component names can become very long compared to html tags especially with genenrics.

@its-the-shrimp's yew-html-ext supports let-else statments that enable early return.

    html! {
        <ul>
            let Ok(iter) = read_dir(&props.path) else {
                return html!("oops :P")
            };
            for entry in iter {
                let Ok(entry) = entry else {
                    return html!("oops :p")
                };
                <li>{ format!("{:?}", entry.path()) }</li>
            }
        </ul>
    }

It also supports cfg attributes on props and components:

    html! {
        <code #[cfg(debug_assertions)] style="color: green;">
            { "Make sure this is not green" }
        </code>
    }

Checklist

  • I have reviewed my own code
  • I have added tests

@Madoshakalaka Madoshakalaka added the A-yew-macro Area: The yew-macro crate label Apr 6, 2026
@Madoshakalaka Madoshakalaka changed the title feat(yew-macro): add first-class match and let bindings in for bodies to html! macro feat: add first-class match and let bindings in for bodies to html! macro Apr 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 6, 2026

Visit the preview URL for this PR (updated for commit c092e39):

https://yew-rs--pr4118-feat-html-match-and-y4khdt3h.web.app

(expires Thu, 16 Apr 2026 13:43:00 GMT)

🔥 via Firebase Hosting GitHub Action 🌎

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 6, 2026

Size Comparison

Details
examples master (KB) pull request (KB) diff (KB) diff (%)
async_clock 100.018 100.017 -0.001 -0.001%
boids 164.111 163.845 -0.267 -0.162%
communication_child_to_parent 93.443 93.444 +0.001 +0.001%
communication_grandchild_with_grandparent 105.424 105.427 +0.003 +0.003%
communication_grandparent_to_grandchild 101.785 101.781 -0.004 -0.004%
communication_parent_to_child 90.856 90.855 -0.001 -0.001%
contexts 105.671 105.668 -0.003 -0.003%
counter 86.026 85.704 -0.322 -0.375%
counter_functional 88.060 87.716 -0.344 -0.390%
dyn_create_destroy_apps 89.948 89.594 -0.354 -0.394%
file_upload 99.035 99.037 +0.002 +0.002%
function_delayed_input 94.014 94.333 +0.319 +0.340%
function_memory_game 169.670 169.477 -0.193 -0.114%
function_router 398.524 398.580 +0.056 +0.014%
function_todomvc 164.562 164.129 -0.434 -0.263%
futures 234.789 234.856 +0.067 +0.029%
game_of_life 100.359 100.419 +0.060 +0.059%
immutable 258.640 258.715 +0.075 +0.029%
inner_html 80.549 80.548 -0.001 -0.001%
js_callback 109.520 109.251 -0.269 -0.245%
keyed_list 175.976 175.879 -0.097 -0.055%
mount_point 83.918 83.917 -0.001 -0.001%
nested_list 113.113 112.779 -0.334 -0.295%
node_refs 91.434 91.431 -0.003 -0.003%
password_strength 1718.530 1718.531 +0.001 +0.000%
portals 92.918 93.072 +0.154 +0.166%
router 364.921 365.322 +0.401 +0.110%
suspense 113.616 113.134 -0.482 -0.425%
timer 88.164 88.288 +0.124 +0.141%
timer_functional 98.776 98.746 -0.030 -0.031%
todomvc 141.845 141.281 -0.563 -0.397%
two_apps 85.890 85.889 -0.001 -0.001%
web_worker_fib 136.022 136.013 -0.010 -0.007%
web_worker_prime 184.400 184.398 -0.002 -0.001%
webgl 82.691 82.690 -0.001 -0.001%

✅ None of the examples has changed their size significantly.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 6, 2026

Benchmark - SSR

Yew Master

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.213 291.716 291.443 0.192
Hello World 10 474.005 497.535 483.934 8.093
Function Router 10 31618.919 31979.170 31783.741 113.697
Concurrent Task 10 1005.249 1007.696 1006.995 0.770
Many Providers 10 1129.477 1152.746 1139.629 7.266

Pull Request

Details
Benchmark Round Min (ms) Max (ms) Mean (ms) Standard Deviation
Baseline 10 291.385 292.066 291.584 0.206
Hello World 10 502.620 512.608 505.606 2.823
Function Router 10 31822.298 33638.526 32570.444 534.856
Concurrent Task 10 1005.446 1007.335 1006.695 0.578
Many Providers 10 1106.227 1125.510 1117.698 5.723

Comment thread examples/function_delayed_input/src/main.rs Outdated
@Madoshakalaka Madoshakalaka changed the title feat: add first-class match and let bindings in for bodies to html! macro feat: first-class match, let bindings, and bare nodes in html! control flow Apr 6, 2026
@Madoshakalaka Madoshakalaka force-pushed the feat/html-match-and-let-in-for branch from 7bd1d40 to 81b0df8 Compare April 6, 2026 06:04
@Madoshakalaka Madoshakalaka force-pushed the feat/html-match-and-let-in-for branch from 78dc29c to 7956fdc Compare April 9, 2026 04:47
@Madoshakalaka Madoshakalaka changed the title feat: first-class match, let bindings, and bare nodes in html! control flow feat: all-you-can-inline html! overhaul Apr 9, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 9, 2026

Benchmark - core

Yew Master

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.716 ns      │ 2.974 ns      │ 2.722 ns      │ 2.727 ns      │ 100     │ 1000000000

Pull Request

vnode           fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ vnode_clone  2.716 ns      │ 3.418 ns      │ 2.72 ns       │ 2.735 ns      │ 100     │ 1000000000

@Madoshakalaka Madoshakalaka force-pushed the feat/html-match-and-let-in-for branch 3 times, most recently from aab7c90 to 675f427 Compare April 9, 2026 08:24
- Support unbraced match arms and let bindings in match arm bodies
- Support bare literals and expressions in unbraced match arms and for-loops
- Support bare nodes and let bindings in if/else bodies
- Support `let` bindings in `for` bodies
- Automatic root fragment wrapping
- Deny patterns containing unnecessarily nested `html!` macros and unnecessary fragments
- Update examples to use the new syntax
- Document match expressions, let bindings, and bare nodes in html!
@Madoshakalaka Madoshakalaka force-pushed the feat/html-match-and-let-in-for branch from 675f427 to 5180a68 Compare April 9, 2026 08:47
@Madoshakalaka Madoshakalaka marked this pull request as ready for review April 9, 2026 10:31
github-actions[bot]
github-actions Bot previously approved these changes Apr 9, 2026
Copy link
Copy Markdown
Member Author

@Madoshakalaka Madoshakalaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed every single file except for the AI translated i18n website pages. This is a big breaking change for the better.

github-actions[bot]
github-actions Bot previously approved these changes Apr 9, 2026
@Madoshakalaka
Copy link
Copy Markdown
Member Author

adding the performance label to trigger benchmarks as a sanity check because I touched key-related macro.

@Madoshakalaka
Copy link
Copy Markdown
Member Author

Madoshakalaka commented Apr 9, 2026

These two scored the worset but it's pure environmental noise as both branches expand to identical code (checked by md5)

Benchmark suite Current: 3e66768 Previous: 40e7134 Ratio
yew-hooks-v0.23.0-keyed 04_select1k 42 39.5 1.06
yew-v0.23.0-keyed 04_select1k 16.9 14.2 1.19

@Madoshakalaka Madoshakalaka merged commit 3650fc8 into master Apr 9, 2026
39 checks passed
@Madoshakalaka Madoshakalaka deleted the feat/html-match-and-let-in-for branch April 9, 2026 13:55
@Madoshakalaka Madoshakalaka restored the feat/html-match-and-let-in-for branch April 9, 2026 14:02
@Madoshakalaka Madoshakalaka deleted the feat/html-match-and-let-in-for branch April 9, 2026 14:05
shan-shaji pushed a commit to shan-shaji/yew that referenced this pull request Apr 19, 2026
- Support unbraced match arms and let bindings in match arm bodies
- Support bare literals and expressions in unbraced match arms and for-loops
- Support bare nodes and let bindings in if/else bodies
- Support `let` bindings in `for` bodies
- Automatic root fragment wrapping
- Protest against patterns containing unnecessarily nested `html!` macros and unnecessary fragments
- Update examples to use the new syntax
- Document match expressions, let bindings, and bare nodes in html!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-yew-macro Area: The yew-macro crate breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant