Files
Htmx/docs/Components/Skeleton.md
T
2026-05-05 23:55:26 +05:00

5.2 KiB
Raw Blame History

Skeleton

An animated grey placeholder that pulsates while real content is loading. Think of it as a rough pencil sketch of your UI — it shows the user where something will appear so the page feels responsive even before the data is ready.


Quick example

new Skeleton("h-4 w-3/4")   // a loading line of text
new Skeleton("h-10 w-full") // a loading input field
new Skeleton("rounded-full h-12 w-12")  // a loading avatar

All the options

public Skeleton(string classes = "")
Parameter What it does
classes Tailwind classes that control the size and shape of the placeholder.

There are no other parameters. The component itself is just an animated <div> — you shape it entirely through CSS classes.


Real-world examples

A card loading state (avatar + two text lines)

<div class="flex items-center gap-4 p-6">
  $$AvatarSkeleton$$
  <div class="space-y-2 flex-1">
    $$Line1$$
    $$Line2$$
  </div>
</div>
_avatarSkeleton = new Skeleton("rounded-full h-10 w-10");
_line1          = new Skeleton("h-4 w-1/2");
_line2          = new Skeleton("h-4 w-3/4");

HTMX swap: show skeleton immediately, replace with real content

Render the skeleton into a slot. HTMX fires immediately on page load and swaps it with the real content:

<div id="user-list"
     hx-get="/users"
     hx-trigger="load"
     hx-swap="outerHTML">
  $$UserListSkeleton$$
</div>

The skeleton appears instantly; the data loads in the background and replaces it.

A full table loading state

// Stack five skeleton rows to simulate a loading table
var rows = string.Concat(Enumerable.Range(0, 5).Select(_ =>
{
    var w = new System.Buffers.ArrayBufferWriter<byte>();
    new Skeleton("h-8 w-full mb-2").Render(new HtmxRenderContext(w));
    return System.Text.Encoding.UTF8.GetString(w.WrittenSpan);
}));

How it works

Skeleton is a single <div> with animate-pulse (Tailwind's built-in pulsing animation) and bg-muted. You control the shape entirely through the classes parameter — use h-* and w-* for size, and rounded-full for circular shapes like avatars.

  • rounded-full makes a circle — useful for avatar skeletons. Combine with equal h-* and w-* values.
  • The classes parameter replaces the default empty string — provide complete size + spacing classes.
  • For table skeletons, render a Table with each cell containing a Skeleton HTML string (pre-rendered to a string via ArrayBufferWriter<byte>).
  • Do not use Skeleton for truly empty states (no data to show) — use an Alert or empty-state illustration instead.
  • Do not use Skeleton for truly empty states (no data to show) — use an Alert or 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");
  }
}