Skeletons and placeholders
Shimmer · pulse · static
Show the shape before the content
Skeletons tell people what is coming and where. They make waiting feel shorter and stop the layout from jumping when content arrives.
- Use skeletons for the first load of content whose layout you know: lists, cards, tables, profiles, dashboards.
- Match the real layout: same sizes, same spacing, same number of lines (2–3 for paragraphs).
- Replace the whole skeleton at once when data arrives, with a 140ms fade. Don't fill it in piece by piece.
- If loading takes more than ~10s, add a status line or switch to a progress indicator.
Three styles
| Style | Motion | Use |
|---|---|---|
| Shimmer | A soft highlight sweeps left to right every 1.6s (right to left in Arabic) | Default for content areas and cards |
| Pulse | Opacity breathes between 100% and 55% every 1.6s | Dense UI and tables where many shimmers would be busy |
| Static | No motion | Reduced-motion preference, print, screenshots, and placeholders that are not loading |
With prefers-reduced-motion the shimmer stops and skeletons become static; pulse slows down.
Anatomy and tokens
Shapes match the content
| User | Organisation | Models |
|---|---|---|
| Element | Skeleton |
|---|---|
| Text line | 12px high, 6px radius, widths 100% / 80% / 60% (last line shorter) |
| Heading | 20px high, ~55% width |
| Avatar | Circle at the real size |
| Image or chart | Block with the real aspect ratio, 16px radius |
| Button | The button's size and radius, no label |
| Number | 36px high, the width of the expected value |
| Token | Light | Dark |
|---|---|---|
--n-skeleton-base | #ECEEF2 | #1E2640 |
--n-skeleton-shine | rgba(255,255,255,0.65) | rgba(255,255,255,0.06) |
Skeleton to content
Swap on arrival
Ghost elements
Placeholders that invite action
Ghost elements are not loading states. They show where something can go: an empty slot in a dashboard, a drop zone, the next card in a builder. Dashed 1.5px border, transparent fill, caption text with an action.
- Use them for empty slots that the user fills, never for content that is loading.
- One clear action inside: “Add widget”, “Drop files”, “Add a lens”.
Requests
574k
Image placeholders
- While an image loads, show its aspect-ratio box in the skeleton base colour (or the Dune tint for illustrations), never a broken-image icon.
- Low-quality previews may blur in (
--n-filter-blur-md) and sharpen over 220ms. - If an image fails, show the alt text in caption colour inside the box.
Code
<span class="n-skel n-skel--text n-skel--shimmer" style="width:80%"></span>
<span class="n-skel n-skel--circle n-skel--shimmer" style="width:40px;height:40px"></span>
<span class="n-skel n-skel--block n-skel--pulse" style="aspect-ratio:16/9"></span>
<div aria-busy="true" aria-live="polite">… skeleton …</div> <!-- container announces when content arrives -->
<!-- Tailwind -->
<span class="block h-3 w-4/5 rounded-[6px] bg-n-skeleton-base relative overflow-hidden
after:absolute after:inset-0 after:-translate-x-full after:animate-shimmer
after:bg-linear-to-r after:from-transparent after:via-n-skeleton-shine after:to-transparent"></span>Dos and don'ts
Do
- Match the real layout and sizes.
- Swap the whole skeleton at once.
- Use static skeletons for reduced motion.
- Use ghosts only for empty slots the user can fill.
Don't
- Show skeletons for data that loads in under 300ms.
- Shimmer every element on a dense screen at once.
- Use brand colours or gradients in skeletons.
- Keep a skeleton forever: time out into an error.
Last updated on 14 October 2026