Status: future · Priority: medium · Shipped parts: template-literal className, state-driven float/color/inline styles (user docs, proven by examples/dynamic)
State-driven
:stylevalues on keyword (enum) fields and fully-dynamic:classbindings. Everything else dynamic already resolves — this page tracks only the runtime path that remains. No performance emergency: see §3.
className={`header ${theme == "light" ? "bg-white" : "bg-gray-900"}}) — each branch becomes a reactive style effect (user docs, examples/dynamic).style={{ width: bodyWidth, backgroundColor: accent }}) — the float/color emitter arms handle any expression.:class="cond ? 'a' : 'b'") resolve each branch to static styles at build time (analyze_dynamic_class in morph-ir). No runtime selector matching exists — in either flow.parseX helpers exist (CSS::parseDisplay, parseOverflow, … in runtime/cpp/style/css_enums.h) — the emitter side below is the only missing piece.:class="someVar" never applied styles at all — setClassName only tracks the string for the inspector. Nothing re-matches selectors at runtime.State-driven :style values on converted keyword fields. :style="{display: myDisplay}" translates the expression (__st_myDisplay.get()), but both emitters only know the string path — node_emitter.rs ("string" arm) and logic_emitter.rs::emit_style_effects ("string" arm) emit style.display = morph::str(expr), and display is now a CSS::Display enum, so compilation fails. Pre-Kill-Strings this "worked" via runtime string compares (the exact cost that project kills).
No string performance problem exists here — only the compile error above:
| Cost per effect run | Scale | Notes |
|---|---|---|
markDirty + layout + paint of the subtree |
Thousands of ops | Inherent to reactivity; untouched by strings work |
morph::str(expr) conversions |
One small alloc | Pre-existing, inherent (text/attrs are strings) |
Planned parseX wrapper (below) |
~5 integer compares, no alloc | <<1% of the effect — noise next to the relayout |
Dynamic classes cost nothing (compile-time resolved; the fully-dynamic form is a no-op). Remaining runtime strings live elsewhere: text content, attribute values, event payload lookups (Kill-Strings batch 3), dev-mode JSON parsing (cold).
morph-codegen/src/node_emitter.rs:
keyword_value_expr(field, cpp) -> Option<String> — static string literal → enum literal at compile time (garbage → behavioral default, same rule as static emission); anything else → CSS::parseX(morph::str(expr)) per effect run. None for still-string fields (flexBasis), which keep today's path."string" arms use it: node_emitter.rs (build TU) and logic_emitter.rs::emit_style_effects (dev TU). css_val_to_cpp untouched — its callers only pass static class values.None; garbage literal → default literal.display/overflow via :style under both morph dev and morph build.native-cpp.md dynamic-styles paragraph; full verify as usual.:class="var" applying styles. Would need a runtime stylesheet + selector matcher — a feature, not a strings fix, and it never worked before. Separate proposal if demanded.reactive_class via the builder. Item-param mapping exists for codegen-translated paths (translate_js); builder-verbatim paths (reactive_class, conditional conditions) do not map the item variable yet. Same class of gap, same fix shape (thread item scope through the builder frame).| Date | Decision | Rationale |
|---|---|---|
| 2026-09-19 | Defer to a scheduled pass; no perf emergency | Only cost is the compile error on state-driven keyword styles; runtime cost after fix is negligible |
| 2026-09-19 | parseX wrapper over string_view, not pre-parsed units |
Effect-run frequency (state changes) doesn't justify schema churn; matches convert-at-boundary rule loosely (boundary = effect run, not frame) |
Related: Kill All Runtime Strings · C++ / JSX Interop Guide