Skip to content

feat(blade-svelte): Skeleton component#3362

Open
rohankokane-dev wants to merge 1 commit intomasterfrom
feat/svelte/skeleton
Open

feat(blade-svelte): Skeleton component#3362
rohankokane-dev wants to merge 1 commit intomasterfrom
feat/svelte/skeleton

Conversation

@rohankokane-dev
Copy link
Copy Markdown
Contributor

Description

Migrates the React Skeleton component to Svelte 5.

Changes

  • Adds packages/blade-svelte/src/components/Skeleton/
  • Adds packages/blade-core/src/styles/Skeleton/
  • Re-exports Skeleton from packages/blade-svelte/src/components/index.ts
  • Re-exports CSS from packages/blade-core/src/styles/index.ts

Additional Information

Artifacts (in worktree):

  • Discovery report: .cursor/artifacts/Skeleton/discovery-report.md
  • Migration plan: .cursor/artifacts/Skeleton/migration-plan.md
  • Verification report: .cursor/artifacts/Skeleton/verification-report.md
  • Screenshots: .cursor/artifacts/Skeleton/screenshots/

See the verification report for the full validation summary.

Component Checklist

  • Update Component Status Page
  • Perform Manual Testing in Other Browsers
  • Add KitchenSink Story
  • Add Interaction Tests (if applicable)
  • Add changeset

Made with Cursor

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

🦋 Changeset detected

Latest commit: 0a408f1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@razorpay/blade-core Patch
@razorpay/blade-svelte Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

✅ PR title follows Conventional Commits specification.

@codesandbox-ci
Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 0a408f1:

Sandbox Source
razorpay/blade: basic Configuration

opacity: 0;
background-color: var(--interactive-background-gray-default);
animation-name: skeleton-fade-in, skeleton-pulse;
animation-duration: var(--duration-2xgentle), 1320ms;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number for animation duration

The 1320ms here is the sum of --duration-xmoderate (360ms) + --duration-2xgentle (960ms), matching the React PulseAnimation logic. However, this is a magic number that will silently drift if those token values ever change.

Consider using calc(var(--duration-xmoderate) + var(--duration-2xgentle)) instead, which stays in sync with the design tokens automatically:

animation-duration: var(--duration-2xgentle), calc(var(--duration-xmoderate) + var(--duration-2xgentle));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — out of scope for this must-fix pass. Will switch to calc(var(--duration-xmoderate) + var(--duration-2xgentle)) in a follow-up so the duration stays in sync with token changes.

animation-timing-function: var(--easing-standard), var(--easing-standard);
animation-iteration-count: 1, infinite;
animation-direction: normal, alternate;
animation-fill-mode: forwards;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing prefers-reduced-motion media query

The Skeleton animation runs infinitely with no respect for users who have reduced motion enabled. Consider adding:

@media (prefers-reduced-motion: reduce) {
  .skeleton {
    animation: none;
    opacity: 1;
    background-color: var(--interactive-background-gray-default);
  }
}

This is an accessibility best practice for any infinite animation. The React version also lacks this, but the Svelte migration is a good opportunity to add it since this CSS lives in blade-core and would benefit both implementations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — out of scope for this must-fix pass. Good a11y suggestion; will add the reduced-motion media query (and likely backport to React Skeleton CSS too) in a follow-up.

const finalStyle = $derived(combineStyleStrings(ownInlineStyle, styledPropsInlineStyle) || undefined);

const a11yAttrs = makeAccessible({ hidden: true });
const metaAttrs = metaAttribute({ name: MetaConstants.Skeleton, testID });
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

metaAttrs not reactive to testID changes

metaAttrs is computed at initialization time (no $derived), so if testID changes at runtime, the data-testid attribute will not update. Same issue with a11yAttrs, though that one uses a static { hidden: true } so it's fine in practice.

For consistency with the reactive pattern used throughout the rest of this component, consider wrapping in $derived:

const metaAttrs = $derived(metaAttribute({ name: MetaConstants.Skeleton, testID }));

This ensures the component behaves correctly if consumers conditionally toggle testID.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. Matches the existing pattern across blade-svelte components; will revisit holistically rather than per-component.


export const skeletonClass = styles.skeleton;

export type SkeletonFlexProps = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused export: skeletonClass

skeletonClass is exported from skeleton.ts and re-exported from the barrel index.ts, but it's never consumed anywhere in this PR (nor anywhere else in the codebase). The getSkeletonClasses() function already returns the base class via the CVA call internally.

If this is intended as a public escape hatch for consumers who need the raw class name, that's fine -- but consider documenting the use case or removing it to keep the public API surface minimal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — out of scope for this must-fix pass. Will remove (or document an external consumer for) the export in a follow-up.

const styledPropsInlineStyle = $derived.by(() => {
const entries = Object.entries(styledProps.inlineStyles ?? {});
if (entries.length === 0) return undefined;
return entries.map(([key, value]) => `${kebabCase(key)}: ${value}`).join('; ');
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated styledPropsInlineStyle logic -- consider consolidating

This manual kebab-case conversion of styledProps.inlineStyles into a CSS string is likely duplicated across other Svelte Blade components. If combineStyleStrings already exists as a shared util, consider also adding a shared inlineStylesToString() helper in blade-core to avoid reimplementing this pattern in every component.

Not blocking, but worth tracking if this pattern keeps appearing in the Svelte migration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — out of scope for this must-fix pass. Will extract a shared util in blade-core in a follow-up once two more components hit the same pattern.

return entries.map(([key, value]) => `${kebabCase(key)}: ${value}`).join('; ');
});

const finalStyle = $derived(combineStyleStrings(ownInlineStyle, styledPropsInlineStyle) || undefined);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line length exceeds 100 chars

Nit: this line is quite long. Consider breaking it for readability:

const finalStyle = $derived(
  combineStyleStrings(ownInlineStyle, styledPropsInlineStyle) || undefined,
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — nit, out of scope for this must-fix pass.

<Skeleton
height="65px"
borderRadius="medium"
marginBottom="spacing.3"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stories: consider extracting duplicated skeleton card layout

The Complex and CardExample stories share a large amount of duplicated layout markup (the two-card loading state pattern in Complex is copy-pasted between left and right panels). This makes the stories harder to maintain.

Consider extracting the shared skeleton card pattern into a small helper snippet or a SkeletonCard fragment within the stories file to reduce duplication and improve readability. Not blocking -- stories are not production code -- but it would make future story updates easier.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking — nit, out of scope for this must-fix pass.

Copy link
Copy Markdown
Contributor Author

@rohankokane-dev rohankokane-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skeleton Svelte 5 Migration -- Code Review Summary

Overall: Good migration. The component is well-structured, follows Svelte 5 idioms ($props(), $derived, $derived.by), and correctly mirrors the React Skeleton's behavior. The blade-core extraction of styles is clean and will benefit both React and Svelte consumers.

What looks good

  • Svelte 5 patterns: Correct use of $props() with destructuring + rest, $derived for reactive computations, and $derived.by for the more complex styled props inline style logic.
  • Architecture: Clean separation between blade-core (styles, CVA, utility mappings) and blade-svelte (component). The getSkeletonClasses / getSkeletonInlineStyle split is well thought out.
  • Accessibility: aria-hidden="true" on each skeleton node matches the React implementation and the documented decision (consumers should use aria-busy on the container). Stories demonstrate the aria-busy pattern correctly.
  • Type safety: Props interface is well-documented with JSDoc comments and correctly extends StyledPropsBlade.
  • Animation fidelity: The CSS keyframes match the React styled-components implementation (fade-in then infinite pulse with the same timing curve and delays).

Issues to address

  1. [suggestion] prefers-reduced-motion: The infinite pulse animation has no reduced-motion fallback. This is an accessibility gap. See inline comment for the recommended CSS.

  2. [suggestion] Magic number 1320ms: The pulse duration is hardcoded as 1320ms instead of using calc(var(--duration-xmoderate) + var(--duration-2xgentle)). If token values change, this will silently break.

  3. [nit] metaAttrs reactivity: metaAttribute(...) is called once at init time, not wrapped in $derived. If testID ever changes at runtime, the DOM won't update. Low impact since most consumers set testID statically, but worth fixing for correctness.

Minor / Non-blocking

  1. Unused export skeletonClass: Exported but never consumed. Consider removing or documenting the intended use case.
  2. Duplicated styledPropsInlineStyle pattern: The kebab-case + join logic will likely repeat across every Svelte Blade component. Worth extracting into a shared util at some point.
  3. Stories duplication: The Complex story has copy-pasted layout markup between left/right panels.

Checklist items from PR description

The PR notes several unchecked items (Component Status Page, manual testing in other browsers, KitchenSink story). Please ensure those are tracked before merge.

No blocking issues. The component is correct and ready to ship after addressing the suggestions above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants