ComponentsData & statusSkeleton
On this page
Data & status

Skeleton released

The shape of content that has not arrived, holding the room it is going to take.

  • Import@pacit/components/skeleton
  • Selectorpct-skeleton

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

skeleton.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctSkeleton } from '@pacit/components/skeleton';
ExportedWhat it is
PctSkeletonShapetype'text' | 'block' | 'circle'Which drawing a skeleton is: the lines of a paragraph, one box, or one disc.
PctSkeletoncomponentpct-skeletonA skeleton: the shape of content that has not arrived, drawn where the content will be.
<pct-skeleton [lines]="3" />
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

Shapes

shape="text" draws lines with a shorter last one, the way a paragraph ends; "block" holds a box — an image, a card — at whatever size the container gives it; "circle" holds a disc, sized on either axis, with the other following.

A card that is still coming

The three shapes arranged the way an application arranges them: a disc the avatar's own size, one short line where the name will be, three where the text will be — under ONE busy region. The wait is a fact about the card, not about each placeholder inside it.

API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

A skeleton: the shape of content that has not arrived, drawn where the content will be.

Inputs

NameTypeWhat it does
linesdefault 1numberHow many lines of the surrounding text this stands in for — one by default.
shapedefault 'text'PctSkeletonShapeLines of text, one box, or one disc — text by default.
pauseddefault falsebooleanStop the sheen where it stands — false by default, and the default is the promise.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-skeleton
aria-hiddentrueThe component draws and does not speak. Everything below it is a box with a colour, there is no text anywhere in the template and nothing in it can take focus, so hiding the subtree costs a reader nothing and saves them a description of content that does not exist (req-a11y-built-in, 0050). What a reader needs to hear is aria-busy on the region, and the region is not ours to write on.
data-pct-shapeshape()
data-pct-pausedpaused() ? "" : null
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctSkeletonHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
fill[data-pct-part="fill"]the shimmer that travels it
track[data-pct-part="track"]one line of the shape

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-skeleton-fill-bg{pct.surface}#ffffff · #0f172aColour of the shimmer.
--pct-skeleton-fill-blur6pxBlur radius of the shimmer.
--pct-skeleton-fill-size100%Width of the shimmer.
--pct-skeleton-track-bg{pct.border}#e2e8f0 · #334155Background of the track.
--pct-skeleton-track-height1capHeight of the track.
--pct-skeleton-track-radius{pct.radius.md}8pxCorner radius of the track.
--pct-skeleton-track-width-last60%Width of the track for the last line.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-skeleton-track-bg: #ccfbf1;
  --pct-skeleton-fill-bg: #f0fdfa;
}
Accessibility

Measured, not assumed

Pattern: none — a skeleton is not a widget and announces nothing. It is aria-hidden="true" outright, the second component in the library to be after pct-icon; what a screen reader has to hear is aria-busy on the region whose content is late, which is the consumer's own element. Named in the class JSDoc.

Keyboard

keyeffecttest

Empty by design, and for the strongest reason in the library so far: nothing here can be landed on at all. The subtree is hidden from the accessibility tree, so anything focusable in it would be axe's aria-hidden-focus — apps/sandbox-e2e/src/skeleton.spec.ts asks every element in the component to focus itself and counts zero, in three engines, and tools/check-aria.mjs point 8 refuses the general case at build time (req-api-platform).

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA pattern named in the class JSDoclibs/components/skeleton/src/skeleton.ts
MeasuredKeyboard map tested key by keyapps/sandbox-e2e/src/skeleton.spec.ts — the empty map is the claim, so what is gated is that no element in the component takes focus in any of the three engines
Measuredaxe audit on the component's own sandbox viewapps/sandbox-e2e/src/a11y.spec.ts — /skeleton in SBX_ROUTES, so the WCAG 2.2 AA sweep takes it. aria-hidden-focus is the rule that would bite here, and the subtree holds nothing to land on
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — skeleton-shapes and skeleton-shapes-rtl: three lines with the short last one, a picture's box and a disc
Measuredforced-colors: active — no state carried by colour alonelibs/components/skeleton/src/skeleton.scss — the box keeps its extent through an outline: 1px solid GrayText over background: Canvas, and the sheen is GrayText (not Highlight: a page of waiting cards would be a wall of selections). The reading: apps/sandbox-e2e/src/forced-colors.spec.ts. The mechanism this refuses is a gradient shimmer, dropped outright in that mode
Measuredprefers-reduced-motion — duration from a tokenapps/sandbox-e2e/src/preferences.spec.ts — the sheen reads 5250 ms — three and a half loops of the reduced token, the sheet's factor scaling the axis's answer rather than replacing it (the loop token slows, it does not stop: a still placeholder stops saying anything is coming). No media query stands in the component's sheet
MeasuredSC 2.2.2 — a stop for motion that outlasts five secondsapps/sandbox-e2e/src/skeleton.spec.ts — paused freezes the sheen where it stands and the page can let it go again, in three engines, with the negative control beside it: a second skeleton the page did not name keeps moving. The sheen itself is not capped, and the reason is the component's whole point (0073); the report's row: docs/acr.md, SC 2.2.2
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target. A skeleton takes no pointer and no focus, so the floor req-a11y-touch sets has nothing to apply to
MeasuredSize axis aligned to --pct-control-height-*deliberately not, and one step further than the progress bar: there is no axis of ours at all. A line is 1lh and the bar inside it 1cap, both the browser's arithmetic over the consumer's type — apps/sandbox-e2e/src/skeleton.spec.ts reads the same component at two font sizes and gets two heights with nothing bound
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a dir="rtl" screenshotlibs/components/skeleton/src/skeleton.scss — the short last line is placed by inline-size in a grid row and the sheen travels by inset-inline-start. The screenshot: skeleton-shapes-rtl; the geometry: apps/sandbox-e2e/src/rtl.spec.ts, where the short bar keeps to the reading direction's start edge
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /skeleton in SBX_ROUTES
n/aFormsnot applicable — a placeholder holds no value a form owns and implements no FormValueControl
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row above
MeasuredTokens registered + an entry in contrast.policy.jsonlibs/tokens/src/contrast.policy.json — two entries, both warn with the reason written beside them (a placeholder has to be visible and must not read as content, and the sheen is motion rather than information); libs/tokens/tokens.snapshot.md
n/aStrings through PCT_TEXTSnot applicable — the component draws no text at all. There is no default sentence to translate, and none to override
MeasuredEntrypoint size budgetlibs/components/size.snapshot.md — the ./skeleton row, on @angular/core alone: it carries no ./core. No size input is no PCT_CONFIG to inject, no text is no PCT_TEXTS, no icon is no drawing to import — so what it ships is the component and nothing under it
MeasuredScreen-reader logRead 2026-09-16 from docs/acr/at/ — three readers, whole sandbox. Not reachable by this instrument: a skeleton takes no focus, and /skeleton walks to the demo button and then out to the scaffold. Nothing in the three logs announces a placeholder, which is what is wanted and is not evidence of it — a Tab walk cannot tell silence from absence. VoiceOver spent one stop on its own instructions about entering the web area, which is the reader talking about itself
MeasuredA docs page with live examplesapps/sandbox/src/app/views/skeleton/ (the sandbox view). The published site: /components/skeleton — prerendered, the demo's own source is the code tab
MeasuredUnit + mutationlibs/components/skeleton/src/skeleton.spec.ts — 24 cases; libs/components/mutation.snapshot.md — skeleton.ts at 92.31, zero clock-kills, and both survivors equivalent: isDevMode() forced true, and parsed >= 1 weakened to parsed > 1, which no case can tell apart because the fallback IS the boundary — one line means one line whichever branch answers
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

92.59%mutation score25 of 27 mutants killed
14 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
2colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
10elements in the preview1 listeners · 1 render to settle · 2945 µs in jsdom, held exactly but for the clock

Why it is built this way

The whole component is two answers (0050): it holds the space the content will take — a line is 1lh and the bar inside it 1cap, both computed by the browser out of the consumer's own type — and it says nothing, because there is nothing yet to say.

Known limitations

  • lines does not reach a block or a circle. It counts lines of text, and neither a box nor a disc has any, so shape="block" [lines]="3" draws one box. The alternative — a box three line boxes tall — needs the count inside the stylesheet, and a component may not invent a --pct-… property that is not a token of the skin.
  • No loading input and no content slot. The swap between the placeholder and the content is an @if the consumer already has, written where they can see it; a slot would put projected content inside an aria-hidden host, which is the one violation this component could cause.
  • Nothing is announced, and the region has to be marked by hand. A dev-mode warning is all this repository can do about it — the busy region is the consumer's element, and no static gate here can see their application.
  • No tone and no timeout. A skeleton that turned red after ten seconds would be guessing a threshold that belongs to the application, and the sentence that goes with it is not a placeholder's. paused is the same boundary drawn once more: the page says when a wait has gone on too long for the motion to keep running, because the page is the only side that knows (0073).
  • The sheen is one animation for all the lines at once, started when each bar is drawn. A staggered wave is a per-bar delay a skin can add; the component does not choose one.