Animated loading placeholder components (shimmer / skeleton screens).
Skeleton screens replace content areas while data is loading, preventing
layout shift and providing a smoother perceived loading experience than a
spinner. All components use Tailwind v4 animate-pulse — pure HEEx, no
JavaScript required.
Sub-components
| Component | Purpose |
|---|---|
skeleton/1 | Base rectangle or circle placeholder block |
skeleton_text/1 | Stack of text-line placeholders simulating a paragraph |
skeleton_avatar/1 | Circular placeholder for avatar images |
skeleton_card/1 | Composite card-shaped placeholder (image + text lines) |
Basic shapes
<%!-- Rectangle placeholder (for an image, header, or button) --%>
<.skeleton class="h-4 w-full" />
<%!-- Circle placeholder (for an avatar) --%>
<.skeleton shape={:circle} width="40px" height="40px" />Dashboard card loading skeletons
Replace stat cards with skeletons while data loads:
<%= if @loading do %>
<div class="grid grid-cols-3 gap-4">
<%= for _ <- 1..3 do %>
<.skeleton_card />
<% end %>
</div>
<% else %>
<.metric_grid>
<%= for stat <- @stats do %>
<.stat_card title={stat.title} value={stat.value} />
<% end %>
</.metric_grid>
<% end %>List item skeletons
Use skeleton_avatar/1 with skeleton_text/1 to match a user list:
<%= if @loading do %>
<div class="space-y-4">
<%= for _ <- 1..5 do %>
<div class="flex items-center gap-4">
<.skeleton_avatar size="10" />
<div class="flex-1">
<.skeleton_text lines={2} />
</div>
</div>
<% end %>
</div>
<% else %>
<%= for user <- @users do %>
<.user_row user={user} />
<% end %>
<% end %>Article / blog post skeleton
<div class="space-y-4 max-w-2xl">
<%!-- Hero image area --%>
<.skeleton class="h-56 w-full" />
<%!-- Title --%>
<.skeleton class="h-8 w-3/4" />
<%!-- Author line --%>
<div class="flex items-center gap-2">
<.skeleton_avatar size="8" />
<.skeleton class="h-4 w-32" />
</div>
<%!-- Body paragraphs --%>
<.skeleton_text lines={5} />
<.skeleton_text lines={4} />
</div>Table skeleton
Match the column widths of your real table:
<table>
<thead>...</thead>
<tbody>
<%= if @loading do %>
<%= for _ <- 1..8 do %>
<tr>
<td class="p-4"><.skeleton class="h-4 w-8" /></td>
<td class="p-4"><.skeleton class="h-4 w-40" /></td>
<td class="p-4"><.skeleton class="h-4 w-24" /></td>
<td class="p-4"><.skeleton class="h-4 w-20" /></td>
</tr>
<% end %>
<% else %>
...real rows...
<% end %>
</tbody>
</table>
Summary
Functions
Renders a single animated skeleton placeholder block.
Renders a circular skeleton placeholder for avatar images.
Renders a composite card-shaped skeleton with a header image area and text lines below it.
Renders a form-shaped skeleton with label + input pairs.
Renders a list of skeleton rows simulating a user list, contact list, or feed.
Renders a profile-page-shaped skeleton.
Renders skeleton rows for a table body.
Renders a stack of skeleton lines simulating a paragraph of text.
Functions
Renders a single animated skeleton placeholder block.
Use :rectangle (default) for any block-level placeholder: image areas,
text lines, buttons, or card borders. Use :circle for avatar placeholders.
Size the skeleton with Tailwind classes via :class, or with explicit
pixel/rem values via :width and :height. Both can be used together.
Examples
<%!-- Full-width text line --%>
<.skeleton class="h-4 w-full" />
<%!-- Fixed-size image placeholder --%>
<.skeleton class="h-40 w-full" />
<%!-- Circle with explicit pixel size --%>
<.skeleton shape={:circle} width="48px" height="48px" />Attributes
shape(:atom) - Shape of the placeholder.:rectangle(default) for blocks and text lines;:circlefor avatars. Defaults to:rectangle. Must be one of:rectangle, or:circle.width(:string) - CSS width applied as an inline style (e.g."200px","50%"). Whennil, width is controlled by theclassattribute. Defaults tonil.height(:string) - CSS height applied as an inline style (e.g."48px","2rem"). Whennil, height is controlled by theclassattribute. Defaults tonil.class(:string) - Additional CSS classes merged viacn/1. Use Tailwindh-N w-Nto size the skeleton. Defaults tonil.- Global attributes are accepted. HTML attributes forwarded to the root
<div>element.
Renders a circular skeleton placeholder for avatar images.
The :size attribute maps directly to Tailwind's numeric sizing scale:
| size | output classes | px size |
|---|---|---|
"6" | w-6 h-6 | 24 px |
"8" | w-8 h-8 | 32 px |
"10" | w-10 h-10 | 40 px |
"14" | w-14 h-14 | 56 px |
Example
<%!-- Default 40px avatar skeleton --%>
<.skeleton_avatar />
<%!-- 56px skeleton matching a "lg" avatar --%>
<.skeleton_avatar size="14" />Attributes
size(:string) - Tailwind size number that maps to bothw-Nandh-N. For example,"10"producesw-10 h-10(40px). Common values:"6"(24px),"8"(32px),"10"(40px),"14"(56px). Defaults to"10".class(:string) - Additional CSS classes merged with the size-derived classes. Defaults tonil.
Renders a composite card-shaped skeleton with a header image area and text lines below it.
Useful as a drop-in replacement for product cards, blog post cards, or dashboard stat cards while their content is loading.
Example
<div class="grid grid-cols-3 gap-4">
<%= for _ <- 1..3 do %>
<.skeleton_card />
<% end %>
</div>Attributes
class(:string) - Additional CSS classes applied to the card wrapper<div>. Defaults tonil.
Renders a form-shaped skeleton with label + input pairs.
Useful as a placeholder while a form's initial values are loading (e.g. an edit form waiting for a record to be fetched).
Example
<%= if @loading do %>
<.skeleton_form fields={6} />
<% else %>
<.my_form changeset={@changeset} />
<% end %>Attributes
fields(:integer) - Number of label + input field pairs to render. Defaults to4.show_submit(:boolean) - Whentrue, renders a button skeleton at the bottom. Defaults totrue.class(:string) - Additional CSS classes. Defaults tonil.
Renders a list of skeleton rows simulating a user list, contact list, or feed.
Each row has an optional avatar circle on the left and two text lines (title + subtitle) on the right.
Example
<%= if @loading do %>
<.skeleton_list items={8} />
<% else %>
<%= for user <- @users do %>
<.user_row user={user} />
<% end %>
<% end %>Attributes
items(:integer) - Number of list item rows to render. Defaults to5.show_avatar(:boolean) - Whentrue, each row has a circular avatar placeholder on the left. Defaults totrue.class(:string) - Additional CSS classes for the wrapper. Defaults tonil.
Renders a profile-page-shaped skeleton.
Simulates the typical layout of a user profile: large avatar, name, bio, and a row of stats (posts, followers, following).
Example
<%= if @loading do %>
<.skeleton_profile />
<% else %>
<.user_profile user={@user} />
<% end %>Attributes
class(:string) - Additional CSS classes. Defaults tonil.
Renders skeleton rows for a table body.
Use inside a <tbody> while table data is loading. The :columns attr
controls how many cells to render per row.
Example
<tbody>
<%= if @loading do %>
<.skeleton_table_row columns={5} rows={8} />
<% else %>
<%= for row <- @rows do %>
<tr>...</tr>
<% end %>
<% end %>
</tbody>Attributes
columns(:integer) - Number of cell placeholders per row. Defaults to4.rows(:integer) - Number of rows to render. Defaults to5.class(:string) - Additional CSS classes for the wrapper. Defaults tonil.
Renders a stack of skeleton lines simulating a paragraph of text.
The last line is always rendered at 75% width (w-3/4) to mimic the
natural way text rarely fills the entire last line of a paragraph.
Example
<%!-- Simulates 4 lines of body text --%>
<.skeleton_text lines={4} />Attributes
lines(:integer) - Number of text lines to render. Default:3. The last line is rendered atw-3/4to simulate natural text flow. Defaults to3.class(:string) - Additional CSS classes applied to the wrapper<div>. Defaults tonil.