Swap.Htmx
1.0.6
dotnet add package Swap.Htmx --version 1.0.6
NuGet\Install-Package Swap.Htmx -Version 1.0.6
<PackageReference Include="Swap.Htmx" Version="1.0.6" />
<PackageVersion Include="Swap.Htmx" Version="1.0.6" />
<PackageReference Include="Swap.Htmx" />
paket add Swap.Htmx --version 1.0.6
#r "nuget: Swap.Htmx, 1.0.6"
#:package Swap.Htmx@1.0.6
#addin nuget:?package=Swap.Htmx&version=1.0.6
#tool nuget:?package=Swap.Htmx&version=1.0.6
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
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.4"></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(options =>
{
options.ConfigureEvents(events =>
{
events.On(CartEvents.ItemAdded)
.AlsoUpdate("cart-count", "_CartCount")
.AlsoUpdate("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
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>
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
With zero configuration, the generators scan your .cshtml files:
// Auto-generated from your views
public static class SwapViews
{
public static class Products
{
public const string Index = "Index";
public const string Grid = "_Grid";
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 (.csproj)
Add your views as additional files for the generators to scan:
<ItemGroup>
<AdditionalFiles Include="Views\**\*.cshtml" />
<AdditionalFiles Include="Modules\**\Views\**\*.cshtml" />
</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 |
| 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
- StackExchange.Redis (>= 2.10.1)
-
net8.0
- StackExchange.Redis (>= 2.10.1)
-
net9.0
- StackExchange.Redis (>= 2.10.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.6 | 20 | 12/11/2025 |
| 1.0.5 | 31 | 12/11/2025 |
| 1.0.4 | 59 | 12/10/2025 |
| 1.0.3 | 53 | 12/9/2025 |
| 1.0.2 | 173 | 12/5/2025 |
| 1.0.1 | 680 | 12/2/2025 |
| 1.0.0 | 521 | 11/27/2025 |
| 0.14.0 | 164 | 11/26/2025 |
| 0.13.0 | 163 | 11/26/2025 |
| 0.12.0 | 166 | 11/26/2025 |
| 0.11.4 | 170 | 11/25/2025 |
| 0.11.3 | 175 | 11/24/2025 |
| 0.11.2 | 344 | 11/21/2025 |
| 0.11.1 | 275 | 11/21/2025 |
| 0.11.0 | 300 | 11/21/2025 |
| 0.10.0 | 341 | 11/21/2025 |
| 0.9.1 | 385 | 11/20/2025 |
| 0.9.0 | 379 | 11/20/2025 |
| 0.8.2 | 386 | 11/20/2025 |
| 0.8.1 | 388 | 11/20/2025 |
| 0.8.0 | 379 | 11/20/2025 |
| 0.7.1 | 381 | 11/20/2025 |
| 0.7.0 | 385 | 11/20/2025 |
| 0.6.0 | 386 | 11/20/2025 |
| 0.5.1 | 383 | 11/19/2025 |
| 0.5.0 | 317 | 11/17/2025 |
| 0.4.1 | 321 | 11/17/2025 |
| 0.4.0 | 266 | 11/16/2025 |
| 0.3.5 | 242 | 11/14/2025 |
| 0.3.4 | 281 | 11/12/2025 |
| 0.3.3 | 264 | 11/12/2025 |
| 0.3.2 | 275 | 11/11/2025 |
| 0.3.1 | 189 | 11/6/2025 |
| 0.3.0 | 190 | 11/3/2025 |
| 0.2.0-dev | 120 | 11/1/2025 |
| 0.1.0 | 177 | 10/30/2025 |