# Skullet is the only way to draw a loading skeleton

Every web loading skeleton goes through MeasuredSkeleton, and a test refuses hand-made pulse boxes, the shadcn Skeleton, other skeleton libraries and skeletons on public pages.

Since 2026-11-04 every loading state in the signed-in web app has been measured from a sample of the component it stands in for, with [skullet](https://skullet.lochie.me). That is now a rule, stated in `AGENTS.md`, `CODING_STANDARDS.md` and on [Design patterns](/docs/development/design): a loading skeleton is `MeasuredSkeleton` from `@/platform/skeleton/measured-skeleton`, fed `data` and a `sample` from the feature's `<feature>.samples.ts`. Only `platform/skeleton/` imports `skullet`.

`scripts/web-skeletons.test.mjs` runs in `pnpm test`. It fails when `apps/web/src` or `packages/ui/src` imports `skullet` outside the wrapper, when app code imports a `*Skeleton*` component from a package (the shadcn `Skeleton`, `SidebarMenuSkeleton`, a UI kit's), when a feature or route draws an `animate-pulse` box with a muted fill, and when either `package.json` lists another skeleton library such as `react-loading-skeleton` or `react-content-loader`. It also walks the imports of every route outside `routes/_auth/` and fails if one reaches the wrapper, so the skullet chunk stays off public pages, as the [first-paint rule](/docs/development/web-conventions) (W15) requires. A public page renders its data from the route loader, or a `Spinner` for a pending action. The [Enforcement](/docs/development/enforcement) page and the [Review checklist](/docs/development/review-checklist) list the new check.

The shadcn `skeleton.tsx` stays in `packages/ui`, because the vendored sidebar and the ReUI emoji picker draw their own loading state with it. Skullet has no React Native build, so the Expo app keeps its `SkeletonRows` placeholder.

