<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Robodobdob</title>
    <description>The latest articles on Forem by Robodobdob (@robodobdob).</description>
    <link>https://forem.com/robodobdob</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F166870%2F2f3d3af1-c609-4d7d-9780-eae234f0d771.jpeg</url>
      <title>Forem: Robodobdob</title>
      <link>https://forem.com/robodobdob</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/robodobdob"/>
    <language>en</language>
    <item>
      <title>Solving Problems with HTMX - Theme Switcher</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Mon, 13 Apr 2026 12:19:45 +0000</pubDate>
      <link>https://forem.com/robodobdob/solving-problems-with-htmx-theme-switcher-306f</link>
      <guid>https://forem.com/robodobdob/solving-problems-with-htmx-theme-switcher-306f</guid>
      <description>&lt;p&gt;Dark mode has been all the rage in dev circles for years now but not everyone is on board with it. I fall into the mostly-light-mode-but-for-that-one-app camp. No shade (pun intended) on those who chose the dark side, but it's just not for my old eyes.&lt;/p&gt;

&lt;p&gt;However, I accept that what works for me doesn't for others and vice versa, so how do you accommodate both users in your application?&lt;/p&gt;

&lt;p&gt;In my personal project I took it as a fun exercise to add a dark mode to my app using HTMX to do all the work. The result was surprisingly simple and, I would argue, easier than a SPA.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Objective
&lt;/h2&gt;

&lt;p&gt;This is the application in its light-mode glory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6goarlld1wz3kzp6ggb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk6goarlld1wz3kzp6ggb.png" alt="Note To Self in default theme" width="800" height="1731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we want to achieve is a switch between a light and dark mode theme with a simple toggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consolidate the colours
&lt;/h2&gt;

&lt;p&gt;The first step is to create two root CSS files - &lt;code&gt;colors-light.css&lt;/code&gt; and &lt;code&gt;colors-dark.css&lt;/code&gt; and put them in the &lt;code&gt;wwwroot&lt;/code&gt; folder as static assets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fda1domtse367ytgkbso6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fda1domtse367ytgkbso6.png" alt="Screenshot showing CSS file locations" width="394" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, all the colours that you want to change when a theme is switched, need to be consolidated into these two CSS files as variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* Dark theme colour variables */&lt;/span&gt;
&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--app-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e1e2e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-card-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2a2a3e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-surface-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2a2a3e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-footer-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1a3a4a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e0e0e0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-text-muted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#a0a0b0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-header-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111111&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;--app-hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found the best way to identify the colours you want is to use browser dev tools to pick the elements and then get their colour data.&lt;/p&gt;

&lt;p&gt;Do the same for the &lt;code&gt;colors-light.css&lt;/code&gt; file (with different colours, obviously).&lt;/p&gt;

&lt;p&gt;Now, the existing CSS files where these colours will be used need to be updated to use the CSS variables instead of explicit colours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.note-list-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;app-card-bg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="mi"&gt;.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;app-hover&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;app-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is quite elegant because it means the CSS for you application doesn't need to change, it just needs to reference a CSS variable. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trick is to change the value of the variable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repeat this same process for any CSS that needs to respond to a colour change.&lt;/p&gt;

&lt;p&gt;The final piece is to wire up the theme CSS to your wherever CSS &lt;code&gt;&amp;lt;link..../&amp;gt;&lt;/code&gt; references are added to your application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"theme-stylesheet"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"css/colors-@(_theme).css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to set the default value when the page loads. I use a session variable but you could also use a cookie or some other persistence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;themeSession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HttpContextAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;themeSession&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;NOTE: Your server-side syntax may vary from mine (Razor), but you should be able to work out what you need&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the switcher
&lt;/h2&gt;

&lt;p&gt;Switching the theme requires a control to make the HTMX request and a server endpoint to persist the change and update the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server endpoint
&lt;/h2&gt;

&lt;p&gt;We first need an endpoint that will update our session and return our updated &lt;code&gt;&amp;lt;ThemeSwitcher/&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Toggle the theme session value and return the new stylesheet link for HTMX to swap in-place.&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/toggle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentTheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newTheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentTheme&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"light"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newTheme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RazorComponentResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeSwitcher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
                &lt;span class="n"&gt;Theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newTheme&lt;/span&gt; 
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Theme switch control
&lt;/h2&gt;

&lt;p&gt;Again, use what works for you, but this is my Blazor &lt;code&gt;&amp;lt;ThemeSwitcher/&amp;gt;&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@inject IHttpContextAccessor HttpContextAccessor

&amp;lt;button type="button"
        class="btn btn-link"
        hx-get="/theme/toggle"
        hx-swap="outerHTML"&amp;gt;
    @if (_isDark)
    {
        &amp;lt;Icon Name="sun" /&amp;gt;
    }
    else
    {
        &amp;lt;Icon Name="moon" /&amp;gt;
    }
&amp;lt;/button&amp;gt;

&amp;lt;hx-partial hx-target="#theme-stylesheet" hx-swap="outerHTML"&amp;gt;
    &amp;lt;link id="theme-stylesheet" rel="stylesheet" href=@($"css/colors-{Theme}.css") /&amp;gt;
&amp;lt;/hx-partial&amp;gt;

@code {
    [Parameter]
    public string? Theme { get; set; }

    private bool _isDark =&amp;gt; Theme == "dark";

    protected override void OnInitialized()
    {
        // If the Theme parameter is not set (i.e., we're rendering this component for the first time), load the theme from the session
        if (Theme == null)
        {
            // Get the current theme from the session, defaulting to "light" if not set
            var themeSession = HttpContextAccessor.HttpContext?.Session.GetString("theme");
            Theme = themeSession ?? "light";
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's pretty simple in how it works. It renders a button with HTMX attributes which displays either a sun or moon icon depending on the current theme.&lt;/p&gt;

&lt;p&gt;The current theme will default to &lt;em&gt;light&lt;/em&gt; unless there is a theme stored in session.&lt;/p&gt;

&lt;p&gt;The key piece that makes it work is the &lt;code&gt;&amp;lt;hx-partial&amp;gt;&lt;/code&gt;. Because we gave our &lt;code&gt;&amp;lt;link... /&amp;gt;&lt;/code&gt; an &lt;code&gt;id&lt;/code&gt; attribute, we can target it with an HTMX response. In this case, the HTML fragment will be a fresh &lt;code&gt;&amp;lt;link... /&amp;gt;&lt;/code&gt; with our updated CSS reference.&lt;/p&gt;

&lt;p&gt;The really cool bit is the browser will detect this change and the styles are applied immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Now, we have our CSS themes, endpoint, and switcher component ready, we can put them together.&lt;/p&gt;

&lt;p&gt;In my case, I just dropped it in the &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;        &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;ThemeSwitcher&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/auth/logout"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"logout-form"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Logout&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;            
        &lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which renders like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1a2f9jqbo9wgf8l5ozi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1a2f9jqbo9wgf8l5ozi.png" alt="Footer in light mode" width="800" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's click the switcher:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00jhpn583b75cf44y0bg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F00jhpn583b75cf44y0bg.png" alt="Footer in dark mode" width="800" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This looks cool, but you really have to see it in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faotvoiea2c5f4ygqxgll.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faotvoiea2c5f4ygqxgll.gif" alt="Animated GIF showing light and dark mode toggling" width="332" height="720"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Of course, this just shows two colour modes, but you could have as many colour themes as you want e.g. one for colour blindness. You just need to consolidate your colours and then make them an option in your switcher.&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>web</category>
      <category>css</category>
      <category>solvingwithhtmx</category>
    </item>
    <item>
      <title>HTMX - Rethinking the SPA</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Mon, 23 Mar 2026 10:41:05 +0000</pubDate>
      <link>https://forem.com/robodobdob/htmx-rethinking-the-spa-2fn2</link>
      <guid>https://forem.com/robodobdob/htmx-rethinking-the-spa-2fn2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj54q44g5xa7gvtoouyf4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj54q44g5xa7gvtoouyf4.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  About me
&lt;/h3&gt;

&lt;p&gt;I have been a full-stack developer for over 25 years here in Wellington. The majority of my career has been in the .Net but I have dabbled in various backend and frontend framework in that time. I’ve worked across various industries from education, legal, government, automotive, and healthcare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lunit
&lt;/h3&gt;

&lt;p&gt;I am currently an engineering lead in the Cancer Screening Group of Lunit. We used to be Volpara Health when I joined and then we were became part of South Korean company Lunit. We are a health technology company which provides a AI products for the screening and diagnosis of cancer.&lt;/p&gt;

&lt;p&gt;While I am not an actual time traveler, I once heard software development described as being a time traveler…. Not only do you build for the future, you often also work&lt;br&gt;
the past.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m5iotvfcn5bh4uqsvmp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7m5iotvfcn5bh4uqsvmp.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  SoundsNZ
&lt;/h3&gt;

&lt;p&gt;Let’s go back to the year 2003 and visit SoundsNZ.com. I worked on this site around 1999/2000 but this screenshot from archive.org is largely the same as what I worked on.&lt;/p&gt;

&lt;p&gt;SoundsNZ was New Zealand’s first online music retailer. It was very successful at the time and they did a lot of business through it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;Architecturally, it was a classic, server-side Active Server Pages application with a Microsoft SQL database. Because this was before “ the cloud” existed, we hosted and ran the site on an IBM server in Wellington, unceremoniously in a small room opposite the office kitchen and toilet facilities.&lt;/p&gt;

&lt;p&gt;Classic ASP was a purely server-side Mutliple Page Application architecture which was the ONLY way to build dynamic web applications - dynamic meaning, the content rendered on screen could be different depending on various conditions or data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Server Side Rendering
&lt;/h3&gt;

&lt;p&gt;Because it was server-side, every interaction was limited to one of two things - changing the page by clicking a link or changing some data by submitting a form. This was not a limitation of the MPA architecture, but really a limitation of HTML itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Because we were a hip and modern web design company, we did use Javascript around the site for small uses of interactivity like a select-all / deselect-all for example.&lt;/p&gt;

&lt;p&gt;But Javascript was very much a small part of the main application which was firmly entrenched in server-side rendered pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwsyfluv4q56c83rs9uwv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwsyfluv4q56c83rs9uwv.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Enter the SPA
&lt;/h3&gt;

&lt;p&gt;Then, sometime around the 2010’s we started to get a new architecture to developer web apps with. It was called the Single Page Application (SPA) and it was offered as a solution to the issues with MPAs at the time. MPAs suffered from a confluence of the prevailing internet conditions at the time, as well as patterns that could be be avoided due to browser deficiencies.&lt;/p&gt;
&lt;h3&gt;
  
  
  The promise
&lt;/h3&gt;

&lt;p&gt;The fundamental promise of the SPA was that we could download the frontend portion of our application to the user’s web browser and let it handle the heavy-lifting of assembling and rendering our UI. This would leave the server to only be concerned with sending data to and receiving data from the frontend in the JSON format.&lt;/p&gt;

&lt;p&gt;In a time when internet speeds weren’t great and shifting and entire DOM + assets to the browser every time the user clicked a link, just taking that hit once and then shifting lightweight JSON over the wire was a VERY attractive prospect. Not only did it reduce traffic, it made apps feel faster and more interactive because we avoided the problems of traditional server-side apps such as the dreaded white screen refresh.&lt;/p&gt;
&lt;h3&gt;
  
  
  Growth of the SPA
&lt;/h3&gt;

&lt;p&gt;As the SPA took hold and became the de facto way web applications were built, we started to replace bespoke implementations of the SPA architecture with consistent, and dependable frameworks such as Backbone, KnockoutJS, React, Vue, Svelte, Angular + many, many, more. &lt;/p&gt;

&lt;p&gt;Where the traditional MPA was a thin-client network&lt;br&gt;
application, the SPA swung the pendulum back towards the thick-client.&lt;/p&gt;
&lt;h3&gt;
  
  
  Problems with SPAs
&lt;/h3&gt;

&lt;p&gt;However, it wasn’t without its issues. Developers now needed to effectively manage a larger application across two machines - the server where the code ran to manage&lt;br&gt;
the state and the client where the code had to manage the user. &lt;/p&gt;

&lt;p&gt;This split personality application became worse particularly if the development team were used to a certain&lt;br&gt;
server-side language or platform and then they quickly had to get good at Javascript.&lt;/p&gt;

&lt;p&gt;They also had to now understand new things like JWT tokens and API endpoints. They had to understand REST (I will cover this specific term a bit later). And above all&lt;br&gt;
else, they had to pick a framework that suited them and hope it was the right choice.&lt;/p&gt;
&lt;h3&gt;
  
  
  A return to SSR
&lt;/h3&gt;

&lt;p&gt;Now, this all happened in the 2010-2020 timeframe. But interestingly, there’s been a noticeable shift in the web development industry back towards server-side rendering.&lt;/p&gt;

&lt;p&gt;It is not a complete swing back, but an equilibrium is being found which balances the benefits of the MPA (server-rendering is usually must faster to render HTML) and the benefits of the SPA (an action has almost instant feedback).&lt;/p&gt;

&lt;p&gt;In fact, some frontend frameworks now tout their server-side offerings as the START point to their frameworks. For example, I have been using Blazor since .Net 5. The Blazor Server architecture is founded very much on “the server has first shot at rendering the markup, and we’ll send the SPA bits next”. Likewise, React Server Side Components looks to be a similar pattern - send what the server rendered first (because it’s faster) and then bring the interactivity in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3l74bcxzlribstqw439z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3l74bcxzlribstqw439z.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  HTMX has entered the chat
&lt;/h3&gt;

&lt;p&gt;The pendulum swinging to the middle conveniently bring us to HTMX. I believe HTMX lies at a confluence of server-side speed / client-side interactivity / developer experience. &lt;/p&gt;

&lt;p&gt;HTMX is founded on the fundamental question of why can’t any HTML element trigger an HTTP request or receive an HTML response. There is a more expanded version of the &lt;a href="https://htmx.org" rel="noopener noreferrer"&gt;https://htmx.org&lt;/a&gt; website, but I feel this is the crux of it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The premise of HTMX
&lt;/h3&gt;

&lt;p&gt;The fundamental idea is HTMX lets the browser do what is native to it (render HTML and handle user events), lets the server do what it is good at (manage state and build HTML). &lt;/p&gt;

&lt;p&gt;Given that HTML out of the box is actually quite limited in what it can do, HTMX seeks to introduce the missing bits of HTML that make it truly useful as the “engine of application state” aka HATEOAS. While the state is technically persisted on the server, the hypermedia is how you interact and manipulate it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A database table can be displayed as a table in HTML. &lt;/li&gt;
&lt;li&gt;A button in that table is clicked to remove the row which causes state to change on the server. &lt;/li&gt;
&lt;li&gt;The database table is re-displayed with the updated state&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Library, not a framework
&lt;/h3&gt;

&lt;p&gt;Technically, HTMX is a library and that means there are no dependency graphs or NPM packages or node_modules to worry about. Simply add a CDN or local reference and you have HTMX-ready application. &lt;/p&gt;

&lt;p&gt;It’s also a tiny library, so there are zero concerns about burdening your user with extra load. All the user will get in their browser is the assembled HTML and any supporting assets (HTMX, CSS, additional JS etc.). &lt;/p&gt;
&lt;h3&gt;
  
  
  Locality of Behaviour
&lt;/h3&gt;

&lt;p&gt;Another fundamental of HTMX is the Locality of Behaviour which distills to “The behaviour of a unit of code should be as obvious as possible by looking only at that unit of code”. &lt;/p&gt;

&lt;p&gt;In practice this means you are adding HTMX directives to your markup directly as hx-* attributes. While this may feel like it pollutes markup, it actually makes reasoning on the markup far simpler for a develop to get up to speed with. &lt;/p&gt;

&lt;p&gt;If you look at these examples, while we can infer that getTime() is going to get the time, we don’t know how or where it will do it. We also don’t know where this actual function lives in our codebase.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;onClick=&lt;/span&gt;&lt;span class="s"&gt;”getTime()”&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Get the time&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But, if we take an HTMX approach where we have a time endpoint, we can see that when clicked we are going to hit the endpoint and get a response back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;”/time”&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Get the time&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Progressive enhancement
&lt;/h3&gt;

&lt;p&gt;Adding HTMX to your application does not mean you are now building an HTMX application. It will happily apply small islands of interactivity as required. &lt;/p&gt;

&lt;p&gt;At its most basic usage, you can even just use the hx-boost attribute at a high-level in your DOM. This will give an immediate benefit by using intercepting your normal HTML interactions and then patching the DOM with the response when it comes back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftenz68k9x9vifa3bdrtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftenz68k9x9vifa3bdrtc.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo code
&lt;/h3&gt;

&lt;p&gt;Head over to &lt;a href="https://github.com/robodobdob/NoteToSelfHono" rel="noopener noreferrer"&gt;https://github.com/robodobdob/NoteToSelfHono&lt;/a&gt; and clone the repo to have a play with the demo site. While I am a .Net developer, this application is actually built on the Bun/Hono stack. The README should you get started.&lt;/p&gt;

&lt;p&gt;You will need to set up a database and storage account. You can also just browse the code and I’m sure you will be able to understand how HTMX works. &lt;/p&gt;

&lt;h3&gt;
  
  
  The basics
&lt;/h3&gt;

&lt;p&gt;Fundamentally, pay attention to &lt;code&gt;hx-*&lt;/code&gt; actions and &lt;code&gt;hx-target&lt;/code&gt; attributes. They will tell where the markup in being generated and where it is ending up, respectively. &lt;/p&gt;

&lt;p&gt;Also, look at the browser dev console and watch the network tab as your interact with the application. You will see that interactions only trigger the bare minimum of network traffic. The only things sent over the wire will be requests to HTML and responses with just the HTML you need. There are no reloads of static assets or the entire DOM tree. &lt;/p&gt;

&lt;h3&gt;
  
  
  Swaps
&lt;/h3&gt;

&lt;p&gt;In the dev console, pay attention to the DOM elements because you see flashes of the which elements are changing from the response. HTMX patches your DOM with the response depending on the hx-target attribute. &lt;/p&gt;

&lt;p&gt;You will sometimes notice multiple DOM elements update from a single request. This is call an out-of-band swap and these are done using the &lt;code&gt;hx-partial&lt;/code&gt; tag.  These are fragments that can be added to a response which will be stripped off by HTMX and rendered somewhere else in the DOM. This is especially useful if you have multiple updates that need to apply at the same time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Headers
&lt;/h3&gt;

&lt;p&gt;It’s worth looking at the request and response headers too. HTMX will add request headers by default which can be useful for determining aspects of the request such as which element fired it. You may want to return different HTML depending on which element as trigger, for instance. Response headers can also be used to trigger HTMX events or even plain Javascript event handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Another key benefit to HTMX is the authentication approach. With a server side model, your users can be just use cookie with. Every HTMX request is a standard browser request so it will include cookies on the request. Since the server issues the cookie, it all “just works”. No JWTs to worry about.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSRF and XSS
&lt;/h3&gt;

&lt;p&gt;A criticism levelled at HTMX is vulnerability to these attacks. All modern server frameworks have mitigations for these attacks and you should 100% be using those as if you were just doing any server side rendering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplwqjvjdkxhka9vzgaov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplwqjvjdkxhka9vzgaov.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The story so far
&lt;/h3&gt;

&lt;p&gt;In my experience so far, I believe HTMX provides a best-of-both worlds to web development. You get the benefits or server-built HTML but with the fast interactivity and DOM changes that users expect from a SPA.&lt;/p&gt;

&lt;h3&gt;
  
  
  In defence of SSR
&lt;/h3&gt;

&lt;p&gt;I think it’s also worth covering that just because something is using server-side, it is inherently slow or old-fashioned or expensive. Servers and cloud infrastructure are fast and cheap now. &lt;/p&gt;

&lt;p&gt;Gone are the days of the multi-thousand $$$$ server in the corner of the office. We can now stand up scalable cloud infrastructure for $/month that can serve many, many users.&lt;/p&gt;

&lt;p&gt;This belief of also based on the idea that your application is serving a public audience with a variable user base. There will be thousands upon thousands of internal web tools , that run big business &lt;em&gt;globally&lt;/em&gt;, that will never experience the white-hot load of “Slashdotting”. They will be the tools that are built to service a specific need in that organisation and will likely have a known user base.&lt;/p&gt;

&lt;p&gt;It is also important to consider what the application itself is actually doing. If you are building a highly interactive, realtime application like Figma, then HTMX is not for you. But, if you are buidling another forms-over-data web application like the thousands of other developers, then HTMX is likely to fit the bill.&lt;/p&gt;

&lt;p&gt;This only scratches the surface of what HTMX is and does, so I highly recommend reading the documentation at &lt;a href="https://htmx.org" rel="noopener noreferrer"&gt;https://htmx.org&lt;/a&gt; to get a sense of breadth of functionality on offer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggfmb5ex9luuk8mzijog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggfmb5ex9luuk8mzijog.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quotes
&lt;/h3&gt;

&lt;p&gt;In preparing this talk, I gathered various quotes from around the web about HTMX. These were all unprompted and just things I had seen in passing. &lt;/p&gt;

&lt;p&gt;Of particular interest is the bottom quote which is from an article entitled &lt;a href="https://htmx.org/essays/paris-2024-olympics-htmx-network-automation/" rel="noopener noreferrer"&gt;Building Critical Infrastructure with htmx: Network Automation for the Paris 2024 Olympics&lt;/a&gt;. It is well worth a read because it shows that  HTMX is a solid choice for large, important applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe148kuhgdi3644kdvx6d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe148kuhgdi3644kdvx6d.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful links
&lt;/h3&gt;

&lt;p&gt;Here are some other useful links which will hopefully interest you more into the world of HTMX.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://htmx.org" rel="noopener noreferrer"&gt;https://htmx.org&lt;/a&gt; and &lt;a href="https://four.htmx.org" rel="noopener noreferrer"&gt;https://four.htmx.org&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://hypermedia.systems" rel="noopener noreferrer"&gt;https://hypermedia.systems&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hyperleaflet-earthquakes.fly.dev/" rel="noopener noreferrer"&gt;https://hyperleaflet-earthquakes.fly.dev/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zweiundeins.gmbh/en/methodology/spa-vs-hypermedia-real-world-performance-under-load" rel="noopener noreferrer"&gt;https://zweiundeins.gmbh/en/methodology/spa-vs-hypermedia-real-world-performance-under-load&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=lASLZ9TgXyc" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=lASLZ9TgXyc&lt;/a&gt; (Building the 100 year web service)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://usvsthem.com" rel="noopener noreferrer"&gt;https://usvsthem.com&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmqeegcxnqewx0p3u2j7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flmqeegcxnqewx0p3u2j7.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, this a translation of a post which I found. It’s perhaps a bit extreme, but HTMX does engender strong feelings :-)&lt;/p&gt;

</description>
      <category>web</category>
      <category>frontend</category>
      <category>backend</category>
      <category>htmx</category>
    </item>
    <item>
      <title>Solving Problems with HTMX - Modal Dialogs</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Thu, 25 Dec 2025 11:19:55 +0000</pubDate>
      <link>https://forem.com/robodobdob/native-html-dialogs-with-htmx-50i2</link>
      <guid>https://forem.com/robodobdob/native-html-dialogs-with-htmx-50i2</guid>
      <description>&lt;p&gt;&lt;em&gt;EDIT: I have retitled this post to indicate it’s actually about modals&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are many ways to implement modal popups in web development these days, but they all expect you to import a library and implement  &lt;em&gt;their&lt;/em&gt; approach.&lt;/p&gt;

&lt;p&gt;I am a big fan of using a native approach to interaction wherever possible. It frees you from dependence of third-parties and gives you a cleaner codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Modal
&lt;/h2&gt;

&lt;p&gt;Let's start with the modal element - the html &lt;code&gt;dialog&lt;/code&gt; element. This will give us largely all the functionality that external libraries give you but with some other nice bits. For instance, pressing &lt;code&gt;ESC&lt;/code&gt; will cancel the dialog. You also don't need to worry about z-indexes or some janky DIV that pretends to be a backdrop.&lt;/p&gt;

&lt;p&gt;I will paste the code I am using in a current project and you can hopefully infer the necessary bits you need to know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"utilityModal"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-100 h-100 p-7"&lt;/span&gt; &lt;span class="na"&gt;hx-target:inherited=&lt;/span&gt;&lt;span class="s"&gt;"#utilityModal_content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"position-absolute top-0 end-0 p-2 d-flex"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Working&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"utilityModal_spinner"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-close"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"close"&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"utilityModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"utilityModal_content"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-1 h-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at this code we can point out a few important bits. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dialog&lt;/code&gt; element is the wrapper element. I also add an hx-target at to the root with the new HTMX4 &lt;code&gt;:inherited&lt;/code&gt; modifier. This means all interactions inside the modal will be contained in the modal (unless otherwise specified).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;button... &amp;gt;&lt;/code&gt; is used as a close button for the dialog. You will note I am also using the freshly-minted native &lt;code&gt;command&lt;/code&gt; and &lt;code&gt;commandfor&lt;/code&gt; APIs. These allow you to issue a set of commands to other elements without any additional JavaScript.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;utilityModal_content&lt;/code&gt; div is perhaps the most important part. It is an empty container which will hold the content of the modal when it is opened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trigger
&lt;/h2&gt;

&lt;p&gt;Now we have a modal defined, we need to launch it and put something useful into it. We can do this simply using HTMX syntax we know and love:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-light p-1"&lt;/span&gt; &lt;span class="na"&gt;command=&lt;/span&gt;&lt;span class="s"&gt;"show-modal"&lt;/span&gt; &lt;span class="na"&gt;commandfor=&lt;/span&gt;&lt;span class="s"&gt;"utilityModal"&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/editnote"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#utilityModal_content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Icon&lt;/span&gt; &lt;span class="na"&gt;Name=&lt;/span&gt;&lt;span class="s"&gt;"plus"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if you're not an HTMX dev, you should be able to work out what is going to happen here. Clicking the button will first show the modal using the native &lt;code&gt;command&lt;/code&gt;/&lt;code&gt;commandfor&lt;/code&gt; API. Next, HTMX will kick in and do an &lt;code&gt;hx-get&lt;/code&gt; to the endpoint and put the response into the &lt;code&gt;hx-target&lt;/code&gt; element. And as we know from above, that target is the content of the modal.&lt;/p&gt;

&lt;p&gt;The end result is a DOM like this:&lt;/p&gt;

&lt;p&gt;Before clicking:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpzikmic3w010e0cyuki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpzikmic3w010e0cyuki.png" alt="The dialog element before clicking"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kpni1oft2ei809ghyu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4kpni1oft2ei809ghyu7.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: the top-layer and ::backdrop pseudo-elements are added by the browser and provide hooks for the styling shown below&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Modal Utilities
&lt;/h2&gt;

&lt;p&gt;Now we have a functioning, native modal with content being injected from HTMX, we could just leave it there. But you incur a few oddities that you probably want to deal with.&lt;/p&gt;
&lt;h3&gt;
  
  
  Issue 1
&lt;/h3&gt;

&lt;p&gt;Firstly, because you are injecting markup into the DOM as your &lt;code&gt;hx-target&lt;/code&gt;, it will remain in the DOM until you remove it, refresh the browser, or replace it with some other content.&lt;/p&gt;

&lt;p&gt;If you leave the markup in the DOM, you can get a weird user experience where the modal opens again and it will immediately display the content from the previous open while it waits for updated content. This can be jarring to the user and even confusing. They might think something has gone wrong.&lt;/p&gt;

&lt;p&gt;To counter this, I add a small modal utility .js file which handles some clean up on the modal content when it is closed. Yes, it's not native, but sometimes you just have to use JavaScript :-)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utilityModal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utilityModal_content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this should be pretty self-explanatory. We register an event listed on the DOM and listen for the &lt;code&gt;close&lt;/code&gt; event which will come from the &lt;code&gt;dialog&lt;/code&gt; element closing (either by button click or pressing &lt;code&gt;ESC&lt;/code&gt;). The event handler itself simply wipes the content of the &lt;code&gt;utilityModal_content&lt;/code&gt; element so it is clean the next time it opens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue 2
&lt;/h3&gt;

&lt;p&gt;Now that we have the content being scrubbed on close, the second issue is, how can we close the modal programmatically in response to an event?&lt;/p&gt;

&lt;p&gt;This is actually stunningly simple and we only need a few lines of code and HTMX will do the rest.&lt;/p&gt;

&lt;p&gt;To start, we need our endpoint to return he right things to HTMX so it can close our modal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/deletenote/{noteId:guid}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;noteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;INotesService&lt;/span&gt; &lt;span class="n"&gt;notesService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;notesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetNoteByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;notesService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveNoteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="n"&gt;httpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HX-Trigger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"notes-updated, close-modal"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I am a .Net developer, so this is a C# minimal API endpoint definition, but if you have done any API development in recent years, the syntax again, should be familiar enough.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the code snippet, I return two &lt;code&gt;HX-Trigger&lt;/code&gt; headers. These headers will be intercepted by HTMX on the response and actioned accordingly.&lt;/p&gt;

&lt;p&gt;All, we need to do now is wire up another listener in our utilities JavaScript and we can close (and scrub) the modal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utilityModal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utilityModal_content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close-modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;That's it! You should now have the bones of a HTML modal using the native &lt;code&gt;dialog&lt;/code&gt; element and a couple of simple event listeners. And HTMX, of course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus Styling
&lt;/h2&gt;

&lt;p&gt;Because styling is not everyone's forte, here is the CSS I have used on a few projects now for this modal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nt"&gt;dialog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;aliceblue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;open&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;25px&lt;/span&gt; &lt;span class="m"&gt;50px&lt;/span&gt; &lt;span class="m"&gt;-12px&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;.25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;::backdrop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;.25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="na"&gt;backdrop-filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nn"&gt;#utilityModal_content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tweak it as you like, but this particular snippet will render like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5ded2ohy9v0zlqllbfs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs5ded2ohy9v0zlqllbfs.png" alt="Screenshot of my note-taking app showing the modal dialog styling"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>web</category>
      <category>css</category>
      <category>solvingwithhtmx</category>
    </item>
    <item>
      <title>Blazor WebAssembly with .Net Framework</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Thu, 23 Jan 2025 06:09:03 +0000</pubDate>
      <link>https://forem.com/robodobdob/blazor-webassembly-with-net-framework-3276</link>
      <guid>https://forem.com/robodobdob/blazor-webassembly-with-net-framework-3276</guid>
      <description>&lt;h1&gt;
  
  
  The Problem
&lt;/h1&gt;

&lt;p&gt;You have a legacy application running on .Net Framework. It performs reasonably well despite its age, and it is &lt;br&gt;
unlikely to go out of support with Microsoft anytime soon. But with .NET 8 (and now 9) well-established, there are &lt;br&gt;
many improvements in code quality, security, speed and even new language features that you are restricted from using &lt;br&gt;
with .NET Framework.&lt;/p&gt;

&lt;p&gt;There are various approaches to "upgrading" a .Net Framework application. One method is the "Strangler Pattern" &lt;br&gt;
whereby you carve off a piece of the legacy codebase, rewrite it, and inject it back into the application somehow.&lt;/p&gt;

&lt;p&gt;So, how can we leverage the power of Blazor WASM to take over functionality that is current provided by old technology on .NET Framework?&lt;/p&gt;
&lt;h1&gt;
  
  
  Blazor WebAssembly
&lt;/h1&gt;

&lt;p&gt;Fundamentally, when you run a Blazor WASM application, it is bootstrapped by a Javascript file which in turn downloads the necessary WASM binaries from the compiled application.&lt;/p&gt;

&lt;p&gt;Since the Blazor WASM appears to the browser as like a Javascript application, we should be able to build a Blazor WASM application and then surface it inside the .NET Framework application without much trouble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blazor Runtime Size&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A criticism often leveled at WebAssembly is the size of the download required to run the application and how it makes the app slow. While it is true the debug-mode runtime is around 20MB, the release version is closer to 7MB. It is important to remember this is a one-time download and will be cached by the browser.&lt;/p&gt;

&lt;p&gt;It is also important to remember that many mobile apps have an initial download in the hundreds of MB, so a slight &lt;br&gt;
delay while the browser downloads a runtime ONCE should be more than tolerable for most users.&lt;/p&gt;
&lt;h1&gt;
  
  
  The Solution
&lt;/h1&gt;

&lt;p&gt;Head over to (&lt;a href="https://github.com/two-thirty-seven/BlazorWithFramework" rel="noopener noreferrer"&gt;https://github.com/two-thirty-seven/BlazorWithFramework&lt;/a&gt;) to get the repo code.&lt;/p&gt;

&lt;p&gt;This repo holds a solution that spins up a .Net Framework application and embeds the Blazor application into it.&lt;/p&gt;

&lt;p&gt;NOTE: You will need to be on a Windows machine to run the solution due to it using .Net Framework.&lt;/p&gt;
&lt;h2&gt;
  
  
  BlazorApp
&lt;/h2&gt;

&lt;p&gt;Starting with .NET 6, Blazor introduced the idea of components being available to Javascript interop (&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/js-spa-frameworks?view=aspnetcore-6.0" rel="noopener noreferrer"&gt;Use Razor components in JavaScript apps and SPA frameworks | Microsoft Learn&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This exposes a new method which is the key to making this all work - &lt;code&gt;RegisterForJavaScript&amp;lt;T&amp;gt;(...)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This method allows us to call into the Blazor WASM binaries and render the component out to a specified DOM node. So, if we tell the Blazor app to expose a component, we can then run some Javascript in the Framework app and put that component into the page. :-)&lt;/p&gt;

&lt;p&gt;If you look at &lt;code&gt;program.cs&lt;/code&gt;, you will see two components registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.RootComponents.RegisterForJavaScript&amp;lt;Counter&amp;gt;(identifier: "counter", javaScriptInitializer: "initializeComponent");
builder.RootComponents.RegisterForJavaScript&amp;lt;Weather&amp;gt;(identifier: "weather", javaScriptInitializer: "initializeComponent");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a build-task in the &lt;code&gt;BlazorApp.csproj&lt;/code&gt; file. This will take the build output from that project and copy the assets into the &lt;code&gt;_framework&lt;/code&gt; folder in the FrameworkApp project. Yes, it is confusing, but Blazor WASM apps compile into a &lt;code&gt;_framework&lt;/code&gt; folder for some reason.&lt;/p&gt;

&lt;p&gt;Apart from that, the Blazor app is out-of-the-box from the template.&lt;/p&gt;

&lt;h2&gt;
  
  
  FrameworkApp
&lt;/h2&gt;

&lt;p&gt;This project is where the magic happens. Looking at &lt;code&gt;_Layout.cshtml&lt;/code&gt;, you will see there is a new Javascript block at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    &amp;lt;script src="/_framework/blazor.webassembly.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script type="text/javascript"&amp;gt;
        window.initializeComponent = async (component, parameters) =&amp;gt; {
            const elements = document.querySelectorAll(`[data-component="${component}"]`);
            elements.forEach(element =&amp;gt; {
                var payload = {};
                if (element.innerText !== "") {
                    payload = JSON.parse(element.innerText);
                }
                console.log(payload);
                window.Blazor.rootComponents.add(element, component, payload);
            });
        }
    &amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is actually pretty simple.&lt;/p&gt;

&lt;p&gt;First, it will load the bootstrapping code &lt;code&gt;blazor.webassembly.js&lt;/code&gt; that has been copied into the project from the build step above.&lt;/p&gt;

&lt;p&gt;Next, it adds the &lt;code&gt;initializeComponent&lt;/code&gt; callback function onto the window object and this will be called by the Blazor WASM application when each component is registered on startup.&lt;/p&gt;

&lt;p&gt;Inside this callback, is where we wire up the components to the DOM. We do this by finding all DOM elements which have the specific &lt;code&gt;data-component&lt;/code&gt; attribute. The value of this attribute &lt;strong&gt;must&lt;/strong&gt; match the name given to the component in the &lt;code&gt;RegisterForJavaScript&amp;lt;T&amp;gt;(...)&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;It will then iterate over each element that matches and will then attach the matching Blazor component to that DOM node.&lt;/p&gt;

&lt;p&gt;The result should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydbyo96k9xzs91ofa64i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydbyo96k9xzs91ofa64i.png" alt="screenshot 1" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The "Counter" component is Blazor WASM but the surrounding page is straight ASP.NET Framework 4.8.&lt;/p&gt;

&lt;p&gt;If we inspect the DOM, we can see nothing untoward, just plain markup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1si3qshoomijo4ge8bts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1si3qshoomijo4ge8bts.png" alt="screenshot 2" width="588" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Passing parameters
&lt;/h2&gt;

&lt;p&gt;Rendering components is great, but what if we need to pass some parameters to the component from MVC?&lt;/p&gt;

&lt;p&gt;First off, we can simply add a Parameter to the component in Blazor. In this example, we want to give it the API url to pull data from:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5omyeebz9mgjqcf820y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5omyeebz9mgjqcf820y.png" alt="screenshot 3" width="756" height="69"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To set this value from MVC, we need to pass the properties as a JSON object to the startup script. We can do this by rendering the JSON into the DOM node and then reading it back during the &lt;code&gt;initializeComponent&lt;/code&gt; callback (see above).&lt;/p&gt;

&lt;p&gt;Then if you look at the code added to &lt;code&gt;_Layout.cshtml&lt;/code&gt; below, you can see it parses that JSON string into an object and the hands it over to the component.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzhpttqnyazb9hsmnd0x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmzhpttqnyazb9hsmnd0x.png" alt="screenshot 4" width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final result is this if we run the Blazor app on its own and use the default sample data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaqeu6b2cn1pbxg30fn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsiaqeu6b2cn1pbxg30fn.png" alt="screenshot 5" width="778" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But if we embed it and pass a different URL, we get a different data result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhas0gkvfjbct3pc4z7pg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhas0gkvfjbct3pc4z7pg.png" alt=" " width="734" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Developing Blazor components
&lt;/h1&gt;

&lt;p&gt;You will want to develop and test your Blazor components without having to spin the .Net Framework app.&lt;/p&gt;

&lt;p&gt;If you look at the &lt;code&gt;program.cs&lt;/code&gt; in the BlazorApp, you will see logic which determine the run state of the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (builder.HostEnvironment.IsEnvironment("Development"))
{
    builder.RootComponents.Add&amp;lt;App&amp;gt;("#app");
    builder.RootComponents.Add&amp;lt;HeadOutlet&amp;gt;("head::after");
}
else
{
    builder.RootComponents.RegisterForJavaScript&amp;lt;Counter&amp;gt;(identifier: "counter", javaScriptInitializer: "initializeComponent");
    builder.RootComponents.RegisterForJavaScript&amp;lt;Weather&amp;gt;(identifier: "weather", javaScriptInitializer: "initializeComponent");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;ASPNETCORE_ENVIRONMENT&lt;/code&gt; launch setting is &lt;code&gt;Development&lt;/code&gt;, it will run as a normal Blazor standalone WebAssembly project. You can then host the components in the Blazor pages and test away.&lt;/p&gt;

&lt;p&gt;If the &lt;code&gt;ASPNETCORE_ENVIRONMENT&lt;/code&gt; launch setting is anything else, it will register the components for JavaScript.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;The ability to embed Blazor WASM components into legacy apps allows you to add modern development tooling with a relatively low friction approach.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Persisting Fluxor state in Blazor WASM</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Tue, 29 Aug 2023 20:49:04 +0000</pubDate>
      <link>https://forem.com/robodobdob/persisting-fluxor-state-in-blazor-wasm-2gd2</link>
      <guid>https://forem.com/robodobdob/persisting-fluxor-state-in-blazor-wasm-2gd2</guid>
      <description>&lt;p&gt;If you are using Blazor WebAssembly with Fluxor, you will likely have come across the situation where refreshing the current page wipes out your state and sets it back to defaults. &lt;/p&gt;

&lt;p&gt;This is not unique to Blazor/Fluxor - it happens in React/Redux too - but it’s a behaviour that is guaranteed to frustrate users.&lt;/p&gt;

&lt;p&gt;This is one approach you can take to ensure that a browser refresh doesn't wipe out your Fluxor state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;The following assumes you already have a Blazor WebAssembly project set up with Fluxor. If not, head over to &lt;a href="https://github.com/mrpmorris/Fluxor" rel="noopener noreferrer"&gt;https://github.com/mrpmorris/Fluxor&lt;/a&gt; and get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 - Session storage
&lt;/h2&gt;

&lt;p&gt;First off we need to install Blazored.SessionStorage following the instructions at &lt;a href="https://github.com/Blazored/SessionStorage" rel="noopener noreferrer"&gt;https://github.com/Blazored/SessionStorage&lt;/a&gt;. This gives us the core functionality to read and write objects to the browser’s Session Storage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.Services.AddBlazoredSessionStorageAsSingleton();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 - Add a new action and reducer
&lt;/h2&gt;

&lt;p&gt;At the heart of Fluxor are actions which mutate state via reducers. The beauty of actions is they can pass any object on their properties.&lt;/p&gt;

&lt;p&gt;Normally an action would pass properties specific to a state, but this time we want an action that passes the state itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public record LoadUsersFromStorageAction(UsersState storedState);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that the action is created, we need to mutate the state in the reducer using the state object on the action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ReducerMethod]
        public static UsersState ReduceLoadUsersFromStorageAction(UsersState state, LoadUsersFromStorageAction action)
        {
            return state with
            {
                ErrorMessage = action.storedState.ErrorMessage,
                LoadingStatus = action.storedState.LoadingStatus,
                Users = action.storedState.Users
            };
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3 - Add a new wrapper component
&lt;/h2&gt;

&lt;p&gt;Create a new Razor component called something like &lt;strong&gt;FluxorSessionStorage.razor&lt;/strong&gt; that is going to handle the reads and writes e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@inject IDispatcher _dispatcher;
@inject ISyncSessionStorageService _sessionStorage;
@inject IState&amp;lt;UsersState&amp;gt; _usersState;

@ChildContent

@code {
    protected override void OnInitialized()
    {
        // read
        var savedState = _sessionStorage.GetItem&amp;lt;UsersState&amp;gt;("UsersState");
        if (savedState != null)
        {
            _dispatcher.Dispatch(new LoadUsersFromStorageAction(savedState));
        }

        // write
        _usersState.StateChanged += ((state, e) =&amp;gt;
        {
            var data = ((IState&amp;lt;UsersState&amp;gt;)state).Value;
            _sessionStorage.SetItem&amp;lt;UsersState&amp;gt;("UsersState", data);
        });
    }

    [Parameter] public RenderFragment? ChildContent { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the OnInitialized method is used to wire up the storage methods. These methods are only called when the component is loaded on refresh.&lt;/p&gt;

&lt;p&gt;The component will wrap the entire Blazor app thereby capturing all the Fluxor activity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Fluxor.Blazor.Web.StoreInitializer /&amp;gt;
&amp;lt;CascadingAuthenticationState&amp;gt;
    &amp;lt;FluxorSessionStorage&amp;gt;
        &amp;lt;Router AppAssembly="@typeof(App).Assembly"&amp;gt;
            &amp;lt;Found Context="routeData"&amp;gt;
                &amp;lt;RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /&amp;gt;
                &amp;lt;FocusOnNavigate RouteData="@routeData" Selector="h1" /&amp;gt;
            &amp;lt;/Found&amp;gt;
            &amp;lt;NotFound&amp;gt;
                &amp;lt;PageTitle&amp;gt;Not found&amp;lt;/PageTitle&amp;gt;
                &amp;lt;LayoutView Layout="@typeof(MainLayout)"&amp;gt;
                    &amp;lt;p role="alert"&amp;gt;Sorry, there's nothing at this address.&amp;lt;/p&amp;gt;
                &amp;lt;/LayoutView&amp;gt;
            &amp;lt;/NotFound&amp;gt;
        &amp;lt;/Router&amp;gt;
    &amp;lt;/FluxorSessionStorage&amp;gt;
&amp;lt;/CascadingAuthenticationState&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 - Test it
&lt;/h2&gt;

&lt;p&gt;Now when we run the application, we should see our Fluxor state being persisted in the browser's Session Storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpsdb7pd2x67nqr9hgpmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpsdb7pd2x67nqr9hgpmc.png" alt=" " width="800" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we make any change to the state, we trigger a write to Session Storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz2l2iwa9y87tunyj7p8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz2l2iwa9y87tunyj7p8.png" alt=" " width="800" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Likewise, if we hit F5, we load the state from Session Storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjtqevq8w6tkbqdghdun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzjtqevq8w6tkbqdghdun.png" alt=" " width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>blazor</category>
      <category>fluxor</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Merging web.config with Azure App Configuration</title>
      <dc:creator>Robodobdob</dc:creator>
      <pubDate>Wed, 30 Mar 2022 20:37:22 +0000</pubDate>
      <link>https://forem.com/robodobdob/merging-webconfig-with-azure-app-configuration-5fpe</link>
      <guid>https://forem.com/robodobdob/merging-webconfig-with-azure-app-configuration-5fpe</guid>
      <description>&lt;p&gt;If you're working in the Azure space, you will probably know that Azure App Configuration is the new hotness. It allows for powerful, centralised control of the application configuration making managing local config files redundant.&lt;/p&gt;

&lt;p&gt;However with new hotness often comes the lack of backwards compatibility. Thankfully, Microsoft haven't completely forgotten about those who still have to work with .Net Framework projects.&lt;/p&gt;

&lt;p&gt;I won't go into &lt;em&gt;how&lt;/em&gt; to set up Azure App Configuration with .Net Framework because it is already &lt;a href="https://docs.microsoft.com/en-us/azure/azure-app-configuration/enable-dynamic-configuration-aspnet-netfx" rel="noopener noreferrer"&gt;well documented by Microsoft&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, what I am going to describe is a method of merging legacy config values from your web.config files with config values from Azure App Configuration into a single DI-friendly &lt;em&gt;IConfiguration&lt;/em&gt; class.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goodies
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder();
builder.AddInMemoryCollection(ConfigurationManager.AppSettings.ToDictionary());
builder.AddAzureAppConfiguration(options =&amp;gt;
{
    options.Connect(aacConnectionString)
        .Select(KeyFilter.Any, LabelFilter.Null)
        .ConfigureRefresh(refresh =&amp;gt;
        {
            refresh.Register("TestApp:Sentinel", refreshAll: true)
                    .SetCacheExpiration(new TimeSpan(0, 5, 0));
        });
    _configurationRefresher = options.GetRefresher();
});

_configuration = builder.Build();

protected void Application_BeginRequest(object sender, EventArgs e)
{
    bool result = _configurationRefresher.TryRefreshAsync().GetAwaiter().GetResult();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is largely boileplate code from the link provided above with the addition of&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder.AddInMemoryCollection(ConfigurationManager.AppSettings.ToDictionary())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key here is the extension method which does the conversion&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static IDictionary&amp;lt;string, string&amp;gt; ToDictionary(this NameValueCollection collection)
{
    var dictionary = collection.Cast&amp;lt;string&amp;gt;().ToDictionary(k =&amp;gt; k, v =&amp;gt; collection[v]);

    return dictionary;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, if you inspect the IConfiguration that the builder produces, you will see two providers - InMemoryProvider and AzureAppConfigurationProvider. Thankfully you don't need to specify which provider when retrieving the key value. They can simply be referred to by&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configuration["keyname"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NOTE: config keys that exist in both web.config and Azure App Configuration will be taken as "last wins".&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional hotness
&lt;/h2&gt;

&lt;p&gt;If you are conscious of security, you will probably not be committing secrets like connection strings to your source control. However you need these when developing code, so you store them in an gitignore'd file and reference it like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;appSettings file="appsettings.dev.config"&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The good news is this is totally supported because the ConfigurationManager works out all this stuff before the Application_Start method is triggered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit
&lt;/h2&gt;

&lt;p&gt;This work was inspired by this blog post which pointed the way to .AddInMemoryCollection(...)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://briancaos.wordpress.com/2020/02/14/azure-app-configuration-getting-the-connection-string-from-appsettings-json/" rel="noopener noreferrer"&gt;Azure App Configuration – Getting the connection string from appsettings.json&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azureappconfiguration</category>
      <category>dotnet</category>
      <category>netframework</category>
    </item>
  </channel>
</rss>
