# 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
```csharp
public Skeleton(string classes = "")
```
| Parameter | Description |
|---|---|
| `classes` | Tailwind classes controlling size, shape, and spacing |
---
## Usage examples
### Text line placeholders
```csharp
new Skeleton("h-4 w-3/4 mb-2")
new Skeleton("h-4 w-1/2")
```
### Avatar placeholder
```csharp
new Skeleton("rounded-full h-12 w-12")
```
### Card skeleton loader
```csharp
new Card(
content: """
""")
```
### Full-width block placeholder
```csharp
new Skeleton("h-10 w-full")
```
### HTMX skeleton swap pattern
```html
$$UserListSkeleton$$
```
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`).
- 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`**
```html
Users
$$LoadingSkeleton$$
```
**`Templates/UserListPage.htmx.cs`**
```csharp
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("""""");
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("
");
}
_skeleton = row.ToString().ToUtf8Bytes();
}
private static string SkeletonHtml(string classes)
{
var buf = new System.Buffers.ArrayBufferWriter();
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**
```csharp
// Shell page — renders immediately with skeleton placeholder
[Handler]
[MapGet("/users")]
public static partial class GetUsersShellHandler
{
public record Query();
private static Task 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 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();
table.Render(new HtmxRenderContext(buf));
return Results.Content(
System.Text.Encoding.UTF8.GetString(buf.WrittenSpan), "text/html");
}
}
```