@@ -0,0 +1,185 @@
|
||||
# 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: """
|
||||
<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
|
||||
|
||||
```csharp
|
||||
new Skeleton("h-10 w-full")
|
||||
```
|
||||
|
||||
### HTMX skeleton swap pattern
|
||||
|
||||
```html
|
||||
<!-- 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`**
|
||||
```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");
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user