ee8797c142
Co-authored-by: Copilot <copilot@github.com>
4.8 KiB
4.8 KiB
Skeleton
An animated loading placeholder. Use it in place of real content while data is being fetched or rendered asynchronously. The animation communicates to the user that content is loading.
HTML structure
div.animate-pulse.rounded-md.bg-muted.{classes}
CSS mechanics
| Class | Effect |
|---|---|
animate-pulse |
Tailwind's built-in fade-in/out animation (1.5s loop) |
bg-muted |
Neutral muted background color from the theme |
rounded-md |
Slightly rounded corners |
User-supplied classes |
Control size and shape (e.g. h-4 w-32, h-10 w-full, rounded-full h-12 w-12) |
Constructor signature
public Skeleton(string classes = "")
| Parameter | Description |
|---|---|
classes |
Tailwind classes controlling size, shape, and spacing |
Usage examples
Text line placeholders
new Skeleton("h-4 w-3/4 mb-2")
new Skeleton("h-4 w-1/2")
Avatar placeholder
new Skeleton("rounded-full h-12 w-12")
Card skeleton loader
new Card(
content: """
<div class="flex items-center gap-4">
<!-- Render each Skeleton eagerly to a string or use slot injection -->
</div>
<div class="mt-4 space-y-2">
</div>
""")
Full-width block placeholder
new Skeleton("h-10 w-full")
HTMX skeleton swap pattern
<!-- Shown immediately; HTMX replaces with real content -->
<div id="user-list"
hx-get="/users"
hx-trigger="load"
hx-swap="outerHTML">
$$UserListSkeleton$$
</div>
The page renders the skeleton on initial load; the HTMX request fires immediately and replaces it once the data arrives.
Tips and tricks
- Multiple
Skeletonelements stacked in adiv.space-y-2create a convincing text-block placeholder. rounded-fullmakes a circle — useful for avatar skeletons. Combine with equalh-*andw-*values.- The
classesparameter replaces the default empty string — provide complete size + spacing classes. - For table skeletons, render a
Tablewith each cell containing a Skeleton HTML string (pre-rendered to a string viaArrayBufferWriter<byte>). - Do not use Skeleton for truly empty states (no data to show) — use an
Alertor empty-state illustration instead. - Do not use Skeleton for truly empty states (no data to show) — use an
Alertor empty-state illustration instead.
Complete page example
Templates/UserListPage.htmx
<div class="max-w-3xl mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Users</h1>
<div id="user-list"
hx-get="/users/data"
hx-trigger="load"
hx-swap="outerHTML">
$$LoadingSkeleton$$
</div>
</div>
Templates/UserListPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class UserListPage : UserListPageBase
{
private readonly byte[] _skeleton;
public UserListPage()
{
// Build a table-shaped skeleton: 5 rows × 3 columns
var row = new System.Text.StringBuilder();
for (int i = 0; i < 5; i++)
{
row.Append("""<div class="flex gap-4 py-3 border-b">""");
row.Append(SkeletonHtml("h-4 w-1/3"));
row.Append(SkeletonHtml("h-4 w-1/4"));
row.Append(SkeletonHtml("h-4 w-1/5"));
row.Append("</div>");
}
_skeleton = row.ToString().ToUtf8Bytes();
}
private static string SkeletonHtml(string classes)
{
var buf = new System.Buffers.ArrayBufferWriter<byte>();
new Components.Skeleton(classes).Render(new HtmxRenderContext(buf));
return System.Text.Encoding.UTF8.GetString(buf.WrittenSpan);
}
protected override void RenderLoadingSkeleton(HtmxRenderContext ctx)
=> ctx.Writer.WriteUtf8(_skeleton);
}
GET handlers
// Shell page — renders immediately with skeleton placeholder
[Handler]
[MapGet("/users")]
public static partial class GetUsersShellHandler
{
public record Query();
private static Task<IResult> HandleAsync(Query _, HttpContext ctx, CancellationToken ct)
=> ctx.WriteHtmxPage(new UserListPage(), title: "Users");
}
// Data endpoint — HTMX swaps this in place of the skeleton
[Handler]
[MapGet("/users/data")]
public static partial class GetUsersDataHandler
{
public record Query();
private static async Task<IResult> HandleAsync(
Query _, HttpContext ctx, MongoDbService db, CancellationToken ct)
{
var users = await db.GetAllUsersAsync(ct);
var table = new Components.Table(
headers: new[] { "Name", "Email", "Role" },
rows: users.Select(u => new[]
{
System.Net.WebUtility.HtmlEncode(u.DisplayName ?? ""),
System.Net.WebUtility.HtmlEncode(u.Email),
"user",
}));
var buf = new System.Buffers.ArrayBufferWriter<byte>();
table.Render(new HtmxRenderContext(buf));
return Results.Content(
System.Text.Encoding.UTF8.GetString(buf.WrittenSpan), "text/html");
}
}