Skip to main content

Skeleton items prop, SkeletonGroup removed (DES-127)

Version: 0.2.0 · Type: ✨ Feature

DES-127 (sub issue of DES-124 Skeleton)

Continuation of WEB-889 ([Skeleton] corner radius / color bound to tokens + card-level shimmer sweep)

Problem

The card-level continuous shimmer sweep landed yesterday (2026-06-16) was carried by a standalone SkeletonGroup component, which forced callers to import and nest both Skeleton and SkeletonGroup. Since SkeletonGroup is not yet used by any business code, we are taking the chance to fold the "multi-block continuous shimmer" capability into Skeleton's own items prop, reducing both the public API surface and the nesting overhead.

Changed Files

  • src/components/Skeleton/Skeleton.tsx
  • src/components/Skeleton/SkeletonGroup.tsx (deleted)
  • src/components/Skeleton/skeleton-group-context.ts (deleted)
  • src/components/Skeleton/index.ts
  • src/components/index.ts
  • src/components/ui/skeleton.tsx (comments)
  • src/components/Skeleton/__tests__/Skeleton.test.tsx
  • scripts/a11y/a11y-audit-config.ts
  • packages/design-site/docs/components/atomic/progress-indicators/skeleton.mdx
  • packages/design-site/i18n/zh-CN/.../skeleton.mdx
  • packages/design-site/src/theme/ReactLiveScope/index.tsx

Changes

  • Skeleton gains an items?: SkeletonItem[] prop (SkeletonItem = { className?: string; key?: Key }):
    • Without items → single-block skeleton, behaving exactly as before.
    • With items → the outer div acts as the whole "canvas", rendering multiple blocks per item, with one light sweeping continuously across blocks (the gaps between blocks are not swept); block layout is controlled by Skeleton's own className (flex / grid / gap).
  • Removed the SkeletonGroup component and SkeletonGroupContext, dropped the SkeletonGroup / SkeletonGroupProps exports; added the SkeletonItem type export.
  • Moved the original SkeletonGroup geometry-measurement logic (ResizeObserver + MutationObserver + applyShimmerGeometry) into Skeleton: in items mode the outer canvas measures the child-block offsets, in single-block mode it measures against its own size.
  • The three doc-site demos (card-level shimmer / text rows / card) are switched to the Skeleton items usage; the Props table drops the SkeletonGroup Props section and adds items / SkeletonItem documentation.
  • The offline a11y audit adds a Skeleton (items) multi-block fixture (previously only the single block was registered).
  • Unit tests rewritten accordingly: removed the SkeletonGroup test block, added items multi-block behaviors (render per item, className pass-through, the canvas carries no skeleton visuals, empty array, ref) and the axe assertion for "decorative skeleton placed inside a role="status" loading container".

Notes

  • The shimmer geometry CSS variables and the singleton <style> injection (skeleton-shimmer.ts) stay unchanged; only the measurement entry point moves from SkeletonGroup to Skeleton.
  • Single-block and multi-block share the same useEffect: single-block does not attach a MutationObserver (no child node additions/removals), only multi-block listens for child additions/removals and observes new nodes as they appear.

2026-06-17 Skeleton gains direction and item.children nested layout (added same day)

Changes

  • Skeleton gains direction?: 'horizontal' | 'vertical' (defaults to vertical, only effective in items mode): it automatically adds flex flex-row/flex-col to the canvas, while details like gap / align are still left to className (merged via mergeClass, so className can override, e.g. switching to grid).
  • SkeletonItem gains children?: SkeletonItem[] and direction?: a leaf item (no children) renders one skeleton block; an item with children does not draw a skeleton block and instead acts as a sub-container, recursively laying out its children per its own direction (defaults to vertical). This is used for mixed layouts like "avatar on the left + two stacked rows on the right".
  • Rendering changes to a recursive renderItem; the geometry-measurement logic needs no change — it still uses querySelectorAll(SKELETON_SELECTOR) to grab all leaf skeleton blocks within the canvas, so the nested structure is naturally compatible and one light sweeps continuously across all leaf blocks.
  • Added the SkeletonDirection type export.
  • The doc site adds direction documentation and a "nested layout (avatar + two rows)" demo; the Props table adds direction and SkeletonItem.children/direction.
  • The offline a11y audit's Skeleton (items) fixture is changed to a nested structure (horizontal canvas + vertical sub-container).
  • Unit tests add 7 cases covering direction default/horizontal/className override, nested children (leaf-block count, branch acting as sub-container, sub-container direction default value).

2026-06-17 Fix single-block mode swallowing children + rationale for removing SkeletonGroup (added same day, review follow-up)

Problem

  • After the refactor, Skeleton destructured children out of props, but the single-block mode return <SkeletonUI ... /> did not pass children back. SkeletonProps still extends HTMLAttributes<HTMLDivElement> (so passing children is type-allowed), which caused the children of legacy usage like <Skeleton>content</Skeleton> to be silently dropped — a behavior regression.

Changed Files

  • src/components/Skeleton/Skeleton.tsx
  • src/components/Skeleton/__tests__/Skeleton.test.tsx

Changes

  • Single-block mode adds {children} back, rendering it as the underlying div's child node, restoring the pre-refactor behavior.
  • Unit tests add a "single-block mode children pass-through" assertion to prevent another regression.

Rationale for removing SkeletonGroup without a deprecated path

Review raised that "SkeletonGroup is already exported from the top level of @plaud/design, making it public API; removal should keep a deprecated compatibility wrapper for one version." Based on the facts of this repo, this change confirms direct removal with no compatibility layer, on the following grounds:

  • SkeletonGroup landed on 2026-06-16, survived for less than 24 hours, and a repo-wide grep finds zero business references.
  • @plaud/design is an internal source package within the monorepo (consumers go directly through src/index.ts), so there is no published contract with unknown external consumers.
  • Therefore it is classified as an "internal adjustment to an unreleased capability", not counted as a breaking change, and needs no deprecated wrapper or major bump.