skeleton-architecture · git:20260219.3b4059d · 2026-02-19 · sha256 5c9e7b35bcfd3955

skeleton-architecture git:20260219.3b4059dA

Immutable. This exact content is served forever at /api/v1/blob/5c9e7b35bcfd3955.

---
description: Skeleton component architecture and consistency rules
globs: src/components/**/*
alwaysApply: false
---

# Skeleton Architecture Rules

Use a hybrid structure for skeleton loaders to balance reuse and locality.

## Placement Model

### Reusable skeletons

Use a standalone component folder when the skeleton is reused by multiple parents or represents a generic UI pattern.

Example:

```
src/components/organisms/FullUserListItemSkeleton/
├── FullUserListItemSkeleton.tsx
├── FullUserListItemSkeleton.test.tsx
└── index.ts
```

### Feature-private skeletons

Use a colocated file for loading states owned by a single component.

Example:

```
src/components/organisms/HotTagsCardsSection/
├── HotTagsCardsSection.tsx
├── HotTagsCardsSection.skeleton.tsx
└── ...
```

## Promotion Rule

- If a colocated `*.skeleton.tsx` is used by 2 or more parent components, promote it to a standalone reusable skeleton component folder.
- If a skeleton remains single-owner, keep it colocated.

## Naming and Exports

- Component name: `XxxSkeleton`.
- Private file name: `Xxx.skeleton.tsx`.
- Reusable file name: `XxxSkeleton.tsx` in its own folder.
- Export reusable skeletons from relevant barrel files.
- Do not export feature-private skeletons from global barrels (`src/components/organisms/index.ts`, etc.).

## Implementation Rules

- Build loaders with shared `Atoms.Skeleton` (Shadcn-based) primitives.
- Avoid ad-hoc placeholder markup that bypasses `Atoms.Skeleton`.
- Keep skeleton layout aligned with the loaded UI structure (spacing, responsive behavior, hierarchy).
- Never hardcode counts or quantities in skeletons. If the loaded component derives a count from a constant or a prop, the skeleton must use the same constant or accept an equivalent prop. Hardcoded literals silently diverge when the source of truth changes.
- If a skeleton count is responsive (varies by viewport or context), accept it as a prop with a sensible constant as the default. The parent already has the computed value at render time and should pass it down.

## Testing Rules

- Reusable skeleton components should have direct unit/snapshot tests.
- Feature-private skeletons should be validated through the parent component loading-state tests.
- Skeleton components that are queried by `data-testid` in parent loading-state tests must own that attribute directly on the rendered element. Never rely on a test mock to inject `data-testid` — the mock should forward props, not hardcode them.

## Quick Checklist

- [ ] Is this skeleton reused by multiple parents? If yes, standalone component.
- [ ] If single-owner, is it colocated as `*.skeleton.tsx`?
- [ ] Does it use `Atoms.Skeleton` consistently?
- [ ] Are exports limited to reusable skeletons only?
- [ ] Are loading states covered by tests (direct or parent-level)?
- [ ] If queried by `data-testid` in a test, does the skeleton component itself set that attribute (not just the mock)?
- [ ] Are all counts/quantities sourced from constants or props — no hardcoded literals?
- [ ] If a count is responsive, is it a prop (with a constant default) passed from the parent?