Swap.Htmx
1.3.0
dotnet add package Swap.Htmx --version 1.3.0
NuGet\Install-Package Swap.Htmx -Version 1.3.0
<PackageReference Include="Swap.Htmx" Version="1.3.0" />
<PackageVersion Include="Swap.Htmx" Version="1.3.0" />
<PackageReference Include="Swap.Htmx" />
paket add Swap.Htmx --version 1.3.0
#r "nuget: Swap.Htmx, 1.3.0"
#:package Swap.Htmx@1.3.0
#addin nuget:?package=Swap.Htmx&version=1.3.0
#tool nuget:?package=Swap.Htmx&version=1.3.0
Swap.Htmx
HTMX + ASP.NET Core, made simple.
Build interactive web apps with server-rendered HTML. No JavaScript frameworks, no complex state management, no build tools.
Quick Start (5 minutes)
1. Install
dotnet add package Swap.Htmx
Optional (realtime + Redis):
dotnet add package Swap.Htmx.Realtime
dotnet add package Swap.Htmx.Realtime.Redis
2. Setup (Program.cs)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddSwapHtmx(); // ← Add this
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseSwapHtmx(); // ← Add this
app.MapControllers();
app.Run();
3. Layout (_Layout.cshtml)
<head>
<link rel="stylesheet" href="~/_content/Swap.Htmx/css/swap.css" />
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
<script src="~/_content/Swap.Htmx/js/swap.client.js"></script>
</head>
<body>
@RenderBody()
</body>
4. Your First Controller
public class ProductsController : SwapController
{
public IActionResult Index()
{
var products = GetProducts();
return SwapView(products); // Auto-detects HTMX vs full page
}
[HttpPost]
public IActionResult Add(Product product)
{
SaveProduct(product);
return SwapResponse()
.WithView("_ProductRow", product) // Main response
.AlsoUpdate("product-count", "_Count", GetCount()) // OOB update
.WithSuccessToast("Product added!") // Toast notification
.Build();
}
}
That's it! You're ready to build interactive UIs.
Core Concepts
1. SwapController & SwapView
SwapController auto-detects HTMX requests:
public class HomeController : SwapController
{
public IActionResult Index()
{
// Normal request → View with layout
// HTMX request → PartialView (no layout)
return SwapView(model);
}
}
Don't want to inherit? Use extensions:
public class HomeController : Controller
{
public IActionResult Index()
{
return this.SwapView(model); // Extension method
}
}
2. SwapResponse Builder
For complex responses with multiple updates:
return SwapResponse()
.WithView("_MainContent", model) // Primary response
.AlsoUpdate("sidebar", "_Sidebar", sidebar) // OOB swap
.AlsoUpdate("header", "_Header", header) // Another OOB
.WithSuccessToast("Done!") // Toast
.WithTrigger("dataUpdated") // Client event
.Build();
3. Swap Navigation
SPA-style navigation without JavaScript:
<a href="/products" hx-get="/products" hx-target="#main" hx-push-url="true">Products</a>
<swap-nav to="/products">Products</swap-nav>
Configure the default target:
builder.Services.AddSwapHtmx(options =>
{
options.DefaultNavigationTarget = "#main-content";
});
4. SwapState (Server-Driven State)
Manage UI state with hidden fields—no JavaScript state management:
Define state:
public class FilterState : SwapState
{
public string Category { get; set; } = "all";
public int Page { get; set; } = 1;
public string? Search { get; set; }
}
Render in view:
<swap-state state="Model.State" />
<input type="text"
name="Search"
value="@Model.State.Search"
hx-get="/Products/Filter?Page=1"
hx-target="#results"
hx-include="#filter-state"
hx-trigger="keyup changed delay:300ms" />
Bind in controller:
[HttpGet]
public IActionResult Filter([FromSwapState] FilterState state)
{
var products = GetProducts(state.Category, state.Search, state.Page);
return PartialView("_ProductList", new ViewModel { State = state, Products = products });
}
Key Pattern: URL parameters override hidden fields (first value wins).
5. Event System
Three approaches, from simple to powerful:
A. Direct Builder (Simple)
// You know exactly what to update
return SwapResponse()
.WithView("_Item", item)
.AlsoUpdate("count", "_Count", count)
.Build();
B. Event Configuration (Medium)
// Configure once in Program.cs
builder.Services.AddSwapHtmx(events =>
{
events.When(CartEvents.ItemAdded)
.RefreshPartial("cart-count", "_CartCount")
.RefreshPartial("cart-total", "_CartTotal");
});
// Controller just fires event
return SwapEvent(CartEvents.ItemAdded, item).WithView("_Added", item).Build();
C. Event Handlers (Powerful) ⭐
// Define events
public static class TaskEvents
{
public static readonly EventKey Completed = new("task.completed");
}
// Handler updates stats (DI supported!)
[SwapHandler(typeof(TaskEvents), nameof(TaskEvents.Completed))]
public class StatsHandler : ISwapEventHandler<TaskPayload>
{
private readonly IStatsService _stats;
public StatsHandler(IStatsService stats) => _stats = stats;
public void Handle(SwapEventContext<TaskPayload> context)
{
var stats = _stats.Calculate();
context.Response.AlsoUpdate("stats-panel", "_Stats", stats);
}
}
// Handler updates activity feed
[SwapHandler(typeof(TaskEvents), nameof(TaskEvents.Completed))]
public class ActivityHandler : ISwapEventHandler<TaskPayload>
{
public void Handle(SwapEventContext<TaskPayload> context)
{
context.Response.AlsoUpdate("activity", "_Activity", GetRecent());
}
}
// Controller stays thin
public IActionResult Complete(int id)
{
var task = _service.Complete(id);
return SwapEvent(TaskEvents.Completed, new TaskPayload(task))
.WithView("_TaskCompleted", task)
.Build();
}
// One event → multiple handlers → one response with all updates
If you’re using SSE/WebSockets, see: 📖 Event Naming & Realtime Routing
6. When to Use OOB Swaps
✅ Use OOB for related updates:
// Add to cart → update count AND total
return SwapResponse()
.WithView("_ProductAdded", product)
.AlsoUpdate("cart-count", "_Count", count)
.AlsoUpdate("cart-total", "_Total", total)
.Build();
❌ Don't stuff unrelated updates:
// BAD: Kitchen sink response
return SwapResponse()
.WithView("_Item", item)
.AlsoUpdate("header", "_Header", header)
.AlsoUpdate("sidebar", "_Sidebar", sidebar)
.AlsoUpdate("footer", "_Footer", footer)
.AlsoUpdate("notifications", "_Notifications", notifications)
// ... 10 more unrelated things
.Build();
Instead: Use event handlers or let components refresh themselves:
<div hx-get="/notifications" hx-trigger="load, every 30s"></div>
7. Performance & Security
OOB swaps render in parallel. When a response contains multiple AlsoUpdate() calls, all partial views are rendered concurrently via Task.WhenAll(). A dashboard with 12+ OOB swaps benefits from this — ordering is preserved.
Target IDs are validated. IDs must start with a letter and contain only letters, digits, hyphens, and underscores. Invalid IDs (XSS payloads, empty strings, special characters) throw ArgumentException at build time, not at render time.
Redirect URLs are validated. WithRedirect() and WithNavigation() reject javascript:, data:, and vbscript: URL schemes.
Feature Reference
| Feature | Usage |
|---|---|
| Auto HTMX detection | SwapView() / this.SwapView() |
| Multiple updates | SwapResponse().AlsoUpdate() |
| SPA navigation | <swap-nav to="/path"> |
| State management | <swap-state> + [FromSwapState] |
| Toast notifications | .WithSuccessToast(), .WithErrorToast() |
| Client events | .WithTrigger("eventName") |
| Event handlers | ISwapEventHandler<T> |
| Form validation | <swap-validation> + SwapValidationErrors() |
| Real-time (SSE) | ServerSentEvents() |
| Real-time (WebSocket) | WebSocket registry |
| Source generators | [SwapEventSource], auto SwapViews/SwapElements |
Source Generators
Eliminate magic strings with compile-time code generation:
1. Type-Safe Event Keys
// Define your events
[SwapEventSource]
public static partial class CartEvents
{
public const string ItemAdded = "cart.itemAdded";
public const string CheckoutCompleted = "cart.checkoutCompleted";
}
// Generated at build time:
// CartEvents.Cart.ItemAdded → EventKey("cart.itemAdded")
// CartEvents.Cart.CheckoutCompleted → EventKey("cart.checkoutCompleted")
// Use in controller
return SwapEvent(CartEvents.Cart.ItemAdded, item).Build();
2. Auto-Generated View & Element Constants (Zero Config)
With zero configuration, the generators scan your .cshtml files and group by controller folder:
// Auto-generated from your views
public static class SwapViews
{
public static class Products
{
public const string Index = "Index";
public const string Details = "Details";
public const string _Grid = "_Grid"; // Partials keep underscore
public const string _Pagination = "_Pagination";
}
}
public static class SwapElements
{
public const string ProductGrid = "product-grid";
public const string CartCount = "cart-count";
}
// Use instead of magic strings
builder.AlsoUpdate(SwapElements.CartCount, SwapViews.Cart._Count, count);
3. Setup (Zero Config!)
No configuration required! The Swap.Htmx.targets auto-includes common folders:
Views/**/*.cshtmlModules/**/Views/**/*.cshtmlPages/**/*.cshtmlComponents/**/*.cshtmlAreas/**/Views/**/*.cshtml
Just reference Swap.Htmx and build — views are scanned automatically.
Optional: To inspect generated code:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>obj\Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="obj\Generated\**\*.cs" />
</ItemGroup>
4. Compile-Time Validation
The HandlerValidationAnalyzer warns you about:
SWAP001: Events without handlersSWAP002: Undefined event keysSWAP003: Circular event chains
📖 Full Source Generators Guide
Documentation
| Guide | Description |
|---|---|
| Getting Started | Full setup walkthrough |
| Public API & Compatibility | What is stable vs experimental |
| Security Checklist | CSRF, realtime auth, room scoping, headers |
| SwapState | Server-driven state management |
| Events | Event system deep dive |
| Navigation | SPA-style navigation |
| Patterns | Common patterns cheatsheet |
| Real-time | SSE & WebSocket |
| Validation | Form validation |
Demos
| Demo | Description |
|---|---|
| SwapStateDemo | State management patterns |
| SwapLab | Pattern showcase |
| SwapShop | E-commerce example |
| SwapDashboard | Dashboard with events |
| SwapSmallPartials | Complex UI orchestration |
Philosophy
- HTML is the source of truth — State lives in the DOM, not JavaScript
- Server renders everything — No client-side templating
- One response, many updates — OOB swaps coordinate UI
- Events decouple UI from logic — Controllers don't know about layout
- No build tools — Just .NET and HTML
License
MIT License - see LICENSE
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Swap.Htmx:
| Package | Downloads |
|---|---|
|
Swap.Htmx.Realtime
Realtime primitives for Swap.Htmx: Server-Sent Events (SSE), WebSockets, connection registry, event bridge, and middleware. (Redis backplane lives in Swap.Htmx.Realtime.Redis.) |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.3.0 | 87 | 3/4/2026 |
| 1.2.0 | 243 | 1/19/2026 |
| 1.1.1 | 212 | 1/13/2026 |
| 1.1.0 | 342 | 12/19/2025 |
| 1.0.6 | 439 | 12/11/2025 |
| 1.0.5 | 428 | 12/11/2025 |
| 1.0.4 | 451 | 12/10/2025 |
| 1.0.3 | 445 | 12/9/2025 |
| 1.0.2 | 276 | 12/5/2025 |
| 1.0.1 | 715 | 12/2/2025 |
| 1.0.0 | 556 | 11/27/2025 |
| 0.14.0 | 199 | 11/26/2025 |
| 0.13.0 | 193 | 11/26/2025 |
| 0.12.0 | 202 | 11/26/2025 |
| 0.11.4 | 196 | 11/25/2025 |
| 0.11.3 | 210 | 11/24/2025 |
| 0.11.2 | 376 | 11/21/2025 |
| 0.11.1 | 301 | 11/21/2025 |
| 0.11.0 | 329 | 11/21/2025 |
| 0.10.0 | 366 | 11/21/2025 |