b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
178 lines
5.2 KiB
Markdown
178 lines
5.2 KiB
Markdown
# 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
|
||
|
||
```csharp
|
||
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
|
||
|
||
```csharp
|
||
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)
|
||
|
||
```html
|
||
<div class="flex items-center gap-4 p-6">
|
||
$$AvatarSkeleton$$
|
||
<div class="space-y-2 flex-1">
|
||
$$Line1$$
|
||
$$Line2$$
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
```csharp
|
||
_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:
|
||
|
||
```html
|
||
<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
|
||
|
||
```csharp
|
||
// 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`**
|
||
```html
|
||
<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`**
|
||
```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("""<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**
|
||
```csharp
|
||
// 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");
|
||
}
|
||
}
|
||
```
|