Components · Feedback
Skeleton
BetaA placeholder while content loads.
- Source
components/ui/skeleton.tsx - shadcn/ui Skeleton
- Figma: not linked yet (Q13)
Clients
Loading clients
When to use
- While a page or region loads content whose shape you know: the client list, a program card, a check-in thread.
- On first load, and when a filter or tab change replaces the whole region.
When not to use
- For an action the coach started, such as saving a meal: show a spinner in the button and keep the content in place.
- For a task with a known length, such as an upload: use Progress.
- For loads that are usually instant (under a second): show nothing, so the page doesn't flash.
- When loading failed or there is nothing to show: replace it with an error Alert or an empty state. A skeleton that never resolves looks like a slow page.
Anatomy
- 1. Shape: matches what it stands for, here a round avatar
- 2. Line: the height of the text it replaces, wider for titles
- 3. Shorter line: secondary text, so the layout reads like the real row
A skeleton is one div. Build the placeholder from several, sized with spacing steps and fractions (h-4 w-3/5), and wrap them in the region that is loading.
Variants
There is one style. Compose it into the shape of the content it replaces.
| Pattern | Shapes |
|---|---|
| List row (client, invoice) | A round avatar and two lines, three to five rows |
| Card (program, workout) | A title line, a shorter description line, and pill shapes for badges |
| Table | One line per cell, header kept as real text |
States
| State | What changes | Token |
|---|---|---|
| Loading | Fill pulses between full and half opacity | bg-accent, animate-pulse |
| Reduced motion | The pulse stops; the fill stays | motion-reduce:animate-none |
| Loaded | The skeleton is replaced by the content in the same place, and aria-busy is removed | none |
The component doesn't stop the pulse on its own. Add motion-reduce:animate-none to each skeleton, as in the examples, until it does.
Usage guidelines
- Match the real layout, so nothing jumps when content arrives: same heights, same gaps, same number of columns.
- Keep real text that is already known, such as page titles, column headers and buttons. Only the data is a skeleton.
- Use three to five rows for a list, not a full page of them.
Content guidelines
A skeleton has no visible text. Its hidden label names what is loading: "Loading clients", "Loading program". Arabic: "جارٍ تحميل العملاء".
Accessibility
Skeletons are not focusable and have no keyboard behavior.
- Hide the shapes from assistive technology with
aria-hidden. They mean nothing read aloud. - Set
aria-busy="true"on the region that is loading, and remove it when the content arrives. - Add a visually hidden label inside the region, such as "Loading clients", so a screen reader user who reaches it knows why it's empty. A screen reader reads "Clients, Loading clients".
- If the coach is waiting on the load, announce the result once in a
role="status"message outside the busy region, such as "18 clients".
RTL
Shapes are placed with the layout, so they follow it: the avatar sits at the start (right in RTL) and lines align to the start. Nothing in the skeleton itself mirrors.
Tokens used
Read from components/ui/skeleton.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --accent | bg-accent | brand-50 | orange-50 |
- Radius
rounded-md
Props
<Skeleton>
Also takes every prop of <div>
No props of its own.
Takes every div prop. Size it with className.
Replaces in coach-admin
From the read-only audit of coach-admin: 90 style values, 115 uses.
coach-admin has a shared skeleton loader and several page-specific ones (settlements, plan cards, calendar tasks, package forms). All of them become compositions of this component.
Classes and selectors
.settlements-skeleton__fill11.skeleton-loader6.plan-card-skeleton__fill6.calendar-task-card__skeleton-title4.calendar-task-card__skeleton-icon3.calendar-task-card__skeleton-sub-text, .calendar-task-card_3.settlements-skeleton__sr-only3.plan-card-skeleton__name3.package-form-skeleton__row--labels2.tasks-review__skeleton-card2.day-column-skeleton2.package-form-skeleton__field-label2
Files
src/views/Settings/PricingTab/plan-card-skeleton.scss13src/reusableComponents/SkeletonLoader/SkeletonLoader.scss4src/views/Program/SmartNutritionPreview/components/ResultsSection/ResultsSectionSkeleton.scss4
The inventory lists the first 12 locations of each value, so file counts are a lower bound.
SCSS variables in these styles
$invoice-section-line→ nearest neutral-200 (no semantic token)$spacing-mdnot a color: spacing, size or layout$light-success-colornot a color: spacing, size or layout$light-warning-colornot a color: spacing, size or layout$spacing-lgnot a color: spacing, size or layout$spacing-xlnot a color: spacing, size or layout