Partials and morph
Real pages rarely update one box at a time. In htmx 4 a single response can update several targets with <hx-partial> and out-of-band swaps, error responses swap by default, and morph swaps update the DOM in place without losing focus or typed input. This guide shows each pattern from Razor Pages and MVC.
<hx-partial>
Section titled “<hx-partial>”One response can update several targets. Server markup (a Razor partial or a controller action returning HTML):
<hx-partial hx-target="#part-a" hx-swap="innerHTML"><span>partial A</span></hx-partial><hx-partial hx-target="#part-b" hx-swap="beforeend"><li>partial B item</li></hx-partial><hx-partial hx-target="#part-nested"><span>partial into nested target</span></hx-partial><div id="part-oob" hx-swap-oob="true">OOB updated</div><div id="part-main-content">main content <span id="part-nested">nested</span></div>- A partial targets via
hx-target(orid) and defaults toinnerHTML. - A response containing only partials leaves the main target untouched.
hx-swap-oobstill works for single-element replacements by id.
In a Razor Page:
// Partials.cshtml.cs — OnGetOnly returns only partialspublic IActionResult OnGetOnly() => Content( "<hx-partial hx-target=\"#part-a\"><span>only partials</span></hx-partial>", "text/html");@* _OrderSaved.cshtml : a reusable multi-target response *@<hx-partial hx-target="#order-count" hx-swap="innerHTML">@Model.Count</hx-partial><hx-partial hx-target="#order-list" hx-swap="beforeend"><li>@Model.Latest</li></hx-partial><div id="toast" hx-swap-oob="true">Saved!</div>Ordering (changed from htmx 2)
Section titled “Ordering (changed from htmx 2)”In htmx 4 the main content swaps first; OOB swaps and <hx-partial> swaps then run in document order. A partial can therefore target an element that exists only after the main swap (#part-nested above). In htmx 2, OOB ran before main. Don’t rely on the old order (htmx 4 reference §8).
Non-200 responses swap by default
Section titled “Non-200 responses swap by default”htmx 4 swaps all responses except 204 and 304. So a 422 validation fragment renders where you expect, with no extra setup:
[HttpGet("status/{code:int}")]public IActionResult Status(int code) => code switch{ // error fragments with an explicit status code 422 => new PartialViewResult { ViewName = "_ValidationError", StatusCode = 422 }, 500 => new PartialViewResult { ViewName = "_Error", StatusCode = 500 }, // not swapped 204 => NoContent(), // not swapped 304 => StatusCode(304), _ => NotFound()};public IActionResult OnPostValidate(string? name){ if (string.IsNullOrWhiteSpace(name)) { Response.StatusCode = StatusCodes.Status422UnprocessableEntity; // swapped by htmx 4 return Partial("_Errors"); } return Partial("_Saved");}Change this behavior with the noSwap setting in the config tag helper (noSwap="@(new object[] { 204, 304, 422 })" stops 422 from swapping) or per element with hx-status. To redirect error content elsewhere, use Retarget/Reswap.
Morph swaps
Section titled “Morph swaps”Morphing updates the existing DOM in place, preserving focus, typed input, scroll and CSS transitions:
// morph the target itselfResponse.Htmx(h => h.Reswap(HtmxSwap.OuterMorph));// morph the target's childrenResponse.Htmx(h => h.Reswap(HtmxSwap.InnerMorph));// morph target attributes, then replace childrenResponse.Htmx(h => h.Reswap(HtmxSwap.OuterSync));You can also set it client-side: hx-swap="outerMorph". The sample’s MVC action:
// DemoController.cs — morph/{kind}case "outer": Response.Htmx(h => h.Reswap(HtmxSwap.OuterMorph)); // _Box renders a div with the same id as the target return Partial("_Box", model);case "inner": Response.Htmx(h => h.Reswap(HtmxSwap.InnerMorph)); // children only return Partial("_Inputs", model);Opt elements out of morphing with the config attributes morphIgnore, morphSkip and morphSkipChildren (Config tag helper), or in markup:
<!-- element left alone by morph --><input hx-morph-skip /><div hx-morph-skip-children>...</div>All HtmxSwap constants
Section titled “All HtmxSwap constants”InnerHtml, OuterHtml, OuterSync, InnerMorph, OuterMorph, TextContent, Delete, None, Before/BeforeBegin, Prepend/AfterBegin, Append/BeforeEnd, After/AfterEnd. None still processes OOB swaps and response headers. Full table: Response headers.
See the live demos: run dotnet run --project samples/Htmx4.Sample and open /Partials (source), /Morph (source) and /ErrorSwaps (source).
Related: Response headers, Config tag helper, htmx 4 reference.
From the blog: HTMX and ASP.NET Core Swapping Techniques compares normal, out-of-band, and document-level swaps.