Files
Htmx/docs/Components/Skeleton.md
T
2026-05-04 19:57:48 +05:00

4.8 KiB
Raw Blame History

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 Skeleton elements stacked in a div.space-y-2 create a convincing text-block placeholder.
  • 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");
  }
}