Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Skeleton loading patterns to display placeholder content while data is being fetched. Reduce perceived load time with animated placeholders.
Skeleton loaders create a visual preview of content that will appear, giving users an immediate sense of the page structure. They are preferable to spinners for content-heavy pages as they maintain layout stability and feel faster.
Key features:
@keyframes for smooth pulse effectaria-busy="true"data-layout="stack" for structureDisplay placeholder lines for text content like paragraphs. Use varying widths to simulate natural text flow, with the last line typically shorter.
<layout-card aria-busy="true" aria-label="Loading content"> <div data-layout="stack" data-layout-gap="s"> <div class="skeleton skeleton-heading"></div> <div class="skeleton skeleton-line"></div> <div class="skeleton skeleton-line"></div> <div class="skeleton skeleton-line"></div> </div></layout-card>
A complete card skeleton with image placeholder, heading, text lines, and button. Use this pattern for product cards, blog posts, or any content card that loads asynchronously.
<layout-card class="card-skeleton" aria-busy="true" aria-label="Loading card"> <div class="skeleton skeleton-image"></div> <div class="card-content" data-layout="stack" data-layout-gap="m"> <div data-layout="stack" data-layout-gap="s"> <div class="skeleton skeleton-heading"></div> <div class="skeleton skeleton-line full"></div> <div class="skeleton skeleton-line medium"></div> <div class="skeleton skeleton-line short"></div> </div> <div class="skeleton skeleton-button"></div> </div></layout-card>
Table skeleton with real headers and placeholder rows. Includes avatar, checkbox, and various cell widths to match typical table layouts. Use this when loading tabular data.
<layout-card data-padding="none" aria-busy="true" aria-label="Loading table data"> <table class="data-table"> <thead> <tr> <th scope="col" class="col-checkbox"></th> <th scope="col">User</th> <th scope="col">Email</th> <th scope="col">Role</th> <th scope="col">Status</th> <th scope="col">Joined</th> </tr> </thead> <tbody> <tr> <td><div class="skeleton skeleton-checkbox"></div></td> <td> <div class="user-cell"> <div class="skeleton skeleton-avatar"></div> <div class="skeleton skeleton-line name"></div> </div> </td> <td><div class="skeleton skeleton-line email"></div></td> <td><div class="skeleton skeleton-line role"></div></td> <td><div class="skeleton skeleton-line status"></div></td> <td><div class="skeleton skeleton-line date"></div></td> </tr> <!-- Additional rows... --> </tbody> </table></layout-card>
The skeleton animation uses a simple pulse effect that fades between two opacity values. This is performant and works across all browsers.
@keyframes skeleton-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; }} .skeleton { background: var(--color-border); border-radius: var(--radius-s); animation: skeleton-pulse 1.5s ease-in-out infinite;} .skeleton-heading { height: 1.75rem; width: 60%;} .skeleton-line { height: 1rem;} .skeleton-line:nth-child(1) { width: 100%; }.skeleton-line:nth-child(2) { width: 100%; }.skeleton-line:nth-child(3) { width: 75%; } .skeleton-image { aspect-ratio: 16 / 9; width: 100%; border-radius: var(--radius-m);} .skeleton-avatar { width: 32px; height: 32px; border-radius: var(--radius-full);} .skeleton-button { height: 2.5rem; width: 8rem; border-radius: var(--radius-m);}
Skeleton elements are built from CSS classes applied to placeholder elements. Here are the key styling options:
| Class | Purpose | Customization |
|---|---|---|
.skeleton |
Base class with animation | Change animation-duration for speed |
.skeleton-heading |
Heading placeholder | Adjust height and width |
.skeleton-line |
Text line placeholder | Set width per line for variety |
.skeleton-image |
Image placeholder | Adjust aspect-ratio to match images |
.skeleton-avatar |
Circular avatar placeholder | Set width/height for size |
.skeleton-button |
Button placeholder | Match your button dimensions |
aria-busy="true" to loading containers. Remove the attribute when content finishes loading.aria-label describing the loading state so screen readers announce meaningful context (e.g. "Loading card").role="status" with a live region announcement when content finishes loading so assistive technology is notified.prefers-reduced-motion by disabling or reducing animations for users who prefer it.@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; opacity: 0.6; }}
You can customize the animation effect to match your brand or preferences:
/* Default pulse animation */@keyframes skeleton-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; }} /* Shimmer/wave animation (left-to-right) */@keyframes skeleton-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; }} .skeleton-shimmer { background: linear-gradient( 90deg, var(--color-border) 0%, var(--color-surface-raised) 50%, var(--color-border) 100% ); background-size: 200% 100%; animation: skeleton-shimmer 1.5s ease-in-out infinite;}
opacity, which is highly performant. Avoid animating layout properties.Unified empty, loading, and error states
When there's no content to display
404, 500, and maintenance pages