Skip to content

feat: Add Skeleton widget#636

Open
Isakdl wants to merge 8 commits intonank1ro:mainfrom
Isakdl:skeleton_widget
Open

feat: Add Skeleton widget#636
Isakdl wants to merge 8 commits intonank1ro:mainfrom
Isakdl:skeleton_widget

Conversation

@Isakdl
Copy link
Copy Markdown
Contributor

@Isakdl Isakdl commented Mar 25, 2026

Changes

Adds the skeleton widget. The widget mimics the React shadcn component as closely as possible, using the same animation curve, timing and coloring.

Demo videos:

Left React component
Right Flutter widget

Note: The background color of the ShadCard in dark mode does not match, but is unrelated to this PR

Screen.Recording.2026-03-25.at.23.02.55.mov
Screen.Recording.2026-03-25.at.23.03.15.mov

Testing this PR

To try this branch, add the following to your pubspec.yaml:

shadcn_ui:
    git:
      url: https://github.com/isakdl/flutter-shadcn-ui
      ref: skeleton_widget

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read and followed the [Flutter Style Guide].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making.
  • I followed the [Data Driven Fixes] where supported.
  • All existing and new tests are passing.
  • I bumped the package version following the Semantic Versioning guidelines (For now the major is the second number and the minor the third, because the package is not feature complete). For example, if the package is at version 0.18.0 and you introduced a breaking change or a new feature, bump it to 0.19.0, if you just added a fix or a chore bump it to 0.18.1.
  • I updated the CHANGELOG.md file with a summary of changes made following the format already used.

Summary by CodeRabbit

  • New Features

    • Added ShadSkeleton — a customizable skeleton loading placeholder with pulsing animation, configurable size, color, radius, and theme support.
  • Documentation

    • Updated README Progress and changelog for version 0.53.0; added documentation link for Skeleton.
  • Examples

    • Added example/playground pages and routes demonstrating Skeleton usage.
  • Tests

    • Added widget tests validating appearance, theming, and sizing.
  • Chores

    • Bumped package version to 0.53.0.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

📝 Walkthrough

Walkthrough

Adds a new ShadSkeleton component with theme integration, exports, example/playground pages and routes, version/changelog/README updates, and widget tests for color and sizing. Theme data, base theme, and variants extended to support skeleton theming.

Changes

Cohort / File(s) Summary
Documentation & Metadata
CHANGELOG.md, README.md, pubspec.yaml
Bumped package to 0.53.0, added changelog entry for ShadSkeleton, and marked Skeleton as completed in README progress.
Library Exports
lib/shadcn_ui.dart
Re-exported src/components/skeleton.dart and src/theme/components/skeleton.dart.
Core Skeleton Component
lib/src/components/skeleton.dart
New ShadSkeleton StatefulWidget with configurable size, color, borderRadius, pulse animation (duration/curve/min opacity), semantics handling, theme lookups, AnimatedBuilder pulsing, animation-disabled fallback, and proper lifecycle management.
Theme: Component & Generated Mixin
lib/src/theme/components/skeleton.dart, lib/src/theme/components/skeleton.g.theme.dart
Added ShadSkeletonTheme (fields: color, borderRadius, pulseHalfDuration, pulseCurve, minPulseOpacity, semanticsLabel, canMerge) and generated _$ShadSkeletonTheme mixin with lerp, copyWith, merge, equality, and hashCode.
Theme: Data & Generated Mixins
lib/src/theme/data.dart, lib/src/theme/data.g.theme.dart
Integrated skeletonTheme into ShadThemeData factory/constructor and generated mixin: added skeletonTheme to lerp, copyWith, merge, equality, and hashCode.
Theme: Base & Variants
lib/src/theme/themes/base.dart, lib/src/theme/themes/default_theme_variant.dart, lib/src/theme/themes/default_theme_no_secondary_border_variant.dart
Added required skeletonTheme field to base theme and implemented skeletonTheme() in default variants with 1s pulseHalfDuration, cubic pulse curve, 0.5 min opacity, and 8px horizontal borderRadius.
Example App
example/lib/main.dart, example/lib/pages/skeleton.dart
Registered '/skeleton' route and added SkeletonPage demonstrating circular avatar skeleton and two text-line skeletons.
Playground App
playground/lib/pages/skeleton.dart, playground/lib/router.dart
Added SkeletonPage with responsive card/layout examples and registered /skeleton route in router.
Tests
test/src/components/skeleton_test.dart
Added three widget tests: theme color usage, constructor color override, and explicit width/height sizing assertions.

Sequence Diagram(s)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #457: Modifies ShadThemeData factory/constructor in lib/src/theme/data.dart — overlaps with skeleton theme integration changes in this PR.

Suggested reviewers

  • nank1ro
  • 9dan

"I hopped through code with a tiny thump,
A pulsing patch where placeholders jump,
Theme threads woven, examples in tow,
Tests verify my soft gray glow—🐇✨"

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add Skeleton widget' clearly and concisely summarizes the main change: introducing a new Skeleton widget component.
Description check ✅ Passed The description covers the main changes, includes demo videos, provides testing instructions, and has nearly all pre-launch checklist items completed (only the issue listing is unchecked, which is acceptable for a straightforward feature addition).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Isakdl Isakdl changed the title Skeleton widget feat: Add Skeleton widget Mar 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/src/components/skeleton.dart (1)

161-187: Optional: reduce duplicated widget tree between static and animated paths.

Both branches rebuild the same SizedBox -> ClipRRect structure. Extracting a tiny helper for the shaped box would simplify maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/components/skeleton.dart` around lines 161 - 187, The
SizedBox->ClipRRect->ColoredBox tree is duplicated in the if/else; extract a
small helper method (e.g., _shapedBox(Color color) or a private Widget
_buildShapedBox(Color color)) that returns the SizedBox with widget.width,
widget.height, ClipRRect(borderRadius: effectiveRadius) and ColoredBox(color:
color), then call that helper from both the static branch (pass effectiveColor)
and the AnimatedBuilder's builder (pass paintColor); keep references to
opacityAnim and effectiveRadius/effectiveColor as-is so behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/src/components/skeleton.dart`:
- Around line 108-116: Normalize the computed pulse config before calling
_ensureAnimation: clamp minOp (the minPulseOpacity variable) into the [0.0, 1.0]
range, and if half (the pulseHalfDuration resolved from widget.pulseHalfDuration
or sk.pulseHalfDuration) is <= Duration.zero replace it with a sensible positive
default (e.g. const Duration(seconds:1) or a small positive Duration), then pass
the sanitized half, curve, and minOp into _ensureAnimation; reference the
variables half, curve, minOp and the _ensureAnimation call when making the
change.
- Around line 175-176: The use of Color.withValues (e.g., the paintColor
assignment that calls effectiveColor.withValues and computes alpha as
(effectiveColor.a * factor).clamp(...)) requires Flutter 3.27.0+; either raise
the package SDK constraint to >=3.27.0 in pubspec.yaml or replace these calls
with a compatible approach such as computing the new opacity and using
effectiveColor.withOpacity((effectiveColor.opacity * factor).clamp(0.0,1.0)) (or
equivalent conversion from effectiveColor.a) everywhere you see
effectiveColor.withValues to ensure runtime compatibility for older SDKs.

---

Nitpick comments:
In `@lib/src/components/skeleton.dart`:
- Around line 161-187: The SizedBox->ClipRRect->ColoredBox tree is duplicated in
the if/else; extract a small helper method (e.g., _shapedBox(Color color) or a
private Widget _buildShapedBox(Color color)) that returns the SizedBox with
widget.width, widget.height, ClipRRect(borderRadius: effectiveRadius) and
ColoredBox(color: color), then call that helper from both the static branch
(pass effectiveColor) and the AnimatedBuilder's builder (pass paintColor); keep
references to opacityAnim and effectiveRadius/effectiveColor as-is so behavior
is unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f451237c-d150-40dc-86ff-95a45dd6b4c2

📥 Commits

Reviewing files that changed from the base of the PR and between 459aabe and 575e20f.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • README.md
  • example/lib/main.dart
  • example/lib/pages/skeleton.dart
  • lib/shadcn_ui.dart
  • lib/src/components/skeleton.dart
  • lib/src/theme/components/skeleton.dart
  • lib/src/theme/components/skeleton.g.theme.dart
  • lib/src/theme/data.dart
  • lib/src/theme/data.g.theme.dart
  • lib/src/theme/themes/base.dart
  • lib/src/theme/themes/default_theme_no_secondary_border_variant.dart
  • lib/src/theme/themes/default_theme_variant.dart
  • playground/lib/pages/skeleton.dart
  • playground/lib/router.dart
  • pubspec.yaml
  • test/src/components/skeleton_test.dart

Comment thread lib/src/components/skeleton.dart Outdated
Comment thread lib/src/components/skeleton.dart Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
lib/src/components/skeleton.dart (1)

177-193: Hoist static layout out of AnimatedBuilder to reduce per-frame rebuilds.

Only the fill color changes per tick; wrapping SizedBox/ClipRRect outside the builder avoids rebuilding static structure every frame.

♻️ Suggested refactor
-      core = AnimatedBuilder(
-        animation: opacityAnim,
-        builder: (context, child) {
-          final factor = opacityAnim.value;
-          final paintColor = effectiveColor.withOpacity(
-            (effectiveColor.opacity * factor).clamp(0.0, 1.0),
-          );
-          return SizedBox(
-            width: widget.width,
-            height: widget.height,
-            child: ClipRRect(
-              borderRadius: effectiveRadius,
-              child: ColoredBox(color: paintColor),
-            ),
-          );
-        },
-      );
+      core = SizedBox(
+        width: widget.width,
+        height: widget.height,
+        child: ClipRRect(
+          borderRadius: effectiveRadius,
+          child: AnimatedBuilder(
+            animation: opacityAnim,
+            builder: (context, child) {
+              final factor = opacityAnim.value;
+              final paintColor = effectiveColor.withOpacity(
+                (effectiveColor.opacity * factor).clamp(0.0, 1.0),
+              );
+              return ColoredBox(color: paintColor);
+            },
+          ),
+        ),
+      );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/src/components/skeleton.dart` around lines 177 - 193, The AnimatedBuilder
currently rebuilds the whole layout (SizedBox, ClipRRect) every frame; hoist the
static widgets out of the builder and only rebuild the color; e.g., create the
SizedBox->ClipRRect structure once outside where core is assigned and pass a
child into AnimatedBuilder (or use AnimatedWidget) so the builder only computes
paintColor from opacityAnim/effectiveColor and returns a ColoredBox (or updates
the child's color), referencing opacityAnim, effectiveColor,
widget.width/widget.height, effectiveRadius, ClipRRect, and ColoredBox to locate
the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/src/components/skeleton.dart`:
- Around line 177-193: The AnimatedBuilder currently rebuilds the whole layout
(SizedBox, ClipRRect) every frame; hoist the static widgets out of the builder
and only rebuild the color; e.g., create the SizedBox->ClipRRect structure once
outside where core is assigned and pass a child into AnimatedBuilder (or use
AnimatedWidget) so the builder only computes paintColor from
opacityAnim/effectiveColor and returns a ColoredBox (or updates the child's
color), referencing opacityAnim, effectiveColor, widget.width/widget.height,
effectiveRadius, ClipRRect, and ColoredBox to locate the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 011ff1db-0ffd-4bf8-9f6b-ea8a3caf0213

📥 Commits

Reviewing files that changed from the base of the PR and between 575e20f and 745c6cf.

📒 Files selected for processing (1)
  • lib/src/components/skeleton.dart

Copy link
Copy Markdown
Owner

@nank1ro nank1ro left a comment

Choose a reason for hiding this comment

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

One open question:
Have you considered https://pub.dev/packages/skeletonizer? Basically, the user doesn't have to replicate the visual design of the components manually, it's already handled by the library itself.

Comment on lines +61 to +66
AnimationController? _controller;
CurvedAnimation? _curved;
Animation<double>? _opacityFactor;
Duration? _lastHalfDuration;
Curve? _lastCurve;
double? _lastMinOpacity;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

  1. you should use flutter_animate for animations, they are completely customizable by the user without changing the widget structure.
  2. make variables public, you're already inside a private class _ShadSkeletonState

@Isakdl
Copy link
Copy Markdown
Contributor Author

Isakdl commented Mar 27, 2026

@nank1ro Interesting package, never heard of that before, seems like a pretty solid solution for this type of problem yes. Do you rather promote the use of that instead of a dedicated skeleton widget in this library? Doesn't seem like a bad idea, but if so I would suggest updating the readme / docs to refer to that, alternatively add it as a dependency with a local wrapper, include a default styling to match the shadcn style (and configurability via the theme).

@nank1ro
Copy link
Copy Markdown
Owner

nank1ro commented Mar 27, 2026

@nank1ro Interesting package, never heard of that before, seems like a pretty solid solution for this type of problem yes. Do you rather promote the use of that instead of a dedicated skeleton widget in this library? Doesn't seem like a bad idea, but if so I would suggest updating the readme / docs to refer to that, alternatively add it as a dependency with a local wrapper, include a default styling to match the shadcn style (and configurability via the theme).

the library already has external dependencies, see them at https://mariuti.com/flutter-shadcn-ui/packages/
So i'm not against it, I've used it a lot of time ago and seems pretty solid. Don't know the performance implications btw.

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.

2 participants