Skip to content

feat(ui): implement consistent skeleton loading UI with shimmer animation across key sections#316

Open
shreyamittal239 wants to merge 1 commit intoAOSSIE-Org:mainfrom
shreyamittal239:improved-ux-issue-277
Open

feat(ui): implement consistent skeleton loading UI with shimmer animation across key sections#316
shreyamittal239 wants to merge 1 commit intoAOSSIE-Org:mainfrom
shreyamittal239:improved-ux-issue-277

Conversation

@shreyamittal239
Copy link

@shreyamittal239 shreyamittal239 commented Feb 23, 2026

Description

This PR introduces a consistent and reusable skeleton loading system to improve perceived performance and eliminate layout shifts across the application.

Previously, certain sections displayed blank regions or experienced layout jumps while content, images, or dynamic data were loading. This created a perception of slow or unresponsive behavior, especially on low-bandwidth connections.

This update implements structured skeleton placeholders and lightweight shimmer animations to provide immediate visual feedback and maintain layout stability during loading states.

Fixes #277

Screen.Recording.2026-02-23.234237.mp4

What Was Implemented

  • Route-level loading UI using app/loading.js
  • Reusable skeleton components:
    • SkeletonText (text blocks)
    • SkeletonCard (cards)
    • SkeletonAvatar / SkeletonCircle (avatars)
    • SkeletonButton (buttons)
    • SkeletonImage (images)
  • Subtle shimmer animation for visual feedback
  • Accessibility improvements (ARIA attributes & screen-reader friendly loaders)
  • Dark mode support (prefers-color-scheme)
  • Fully responsive layout support
  • Applied to high-visibility sections:
    • Hero
    • Features
    • TechStack
    • About
    • DownloadApp

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • New release
  • UI/UX update
  • CI/CD & Tooling
  • Dependency update

How Has This Been Tested?

  • Ran npm run dev locally.
  • Verified skeleton loaders appear on initial render (1.5s delay).
  • Confirmed layout remains stable during loading.
  • Tested responsiveness across different screen sizes.
  • Verified dark mode support.
  • Confirmed no console warnings or errors.
  • Confirmed npm run build passes without warnings.

The skeletons correctly render before content loads and are replaced seamlessly once content becomes available.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code where necessary
  • My changes generate no new warnings
  • I have checked my code and corrected misspellings

Summary by CodeRabbit

Release Notes

  • New Features

    • Added skeleton loading screens with animated shimmer effects across all major sections—Hero, Features, About, Download, and Tech Stack—providing visual feedback while content loads.
    • New loading page displays during navigation with spinner and status message.
    • Dark mode support for all loading placeholders.
  • Style

    • Added global theme variables for consistent skeleton UI styling throughout the app.

@shreyamittal239 shreyamittal239 requested a review from a team as a code owner February 23, 2026 18:14
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

This PR implements skeleton loading screens and loading states across the application. It introduces a new Skeleton component library with 8 reusable skeleton UI components, a custom hook for managing loading states with configurable delay, and applies these to 5 major sections (Hero, About, Features, TechStack, DownloadApp). Global CSS variables, dark mode support, and route-level loading UI are also added.

Changes

Cohort / File(s) Summary
Skeleton Component Library
app/components/Skeleton/Skeleton.jsx, app/components/Skeleton/Skeleton.module.css, app/components/Skeleton/index.js, app/components/Skeleton/useSkeletonLoading.js
New skeleton UI library with 8 reusable components (Text, Title, Button, Image, Circle, Card, Row, Icon), comprehensive CSS styling with shimmer animation, dark mode support, and a custom hook managing loading state with 1500ms default delay.
Component Loading States
app/components/Hero/Hero.jsx, app/components/About/About.jsx, app/components/Features/Features.jsx, app/components/DownloadApp/DownloadApp.jsx, app/components/TechStack/TechStack.jsx
Imported and integrated skeleton components and useSkeletonLoading hook; added conditional rendering that displays skeleton UI on initial load, then switches to actual content after delay expires.
Global Styles & Configuration
app/globals.css
Added CSS custom properties for skeleton styling (base, highlight, card backgrounds, radius) and dark mode color overrides via @media query.
Route-Level Loading UI
app/loading.js, app/loading.module.css
New loading component with centered spinner animation and "Loading content..." text for route transitions.
Layout Refactoring
app/layout.js
Extracted viewport configuration from metadata object into a separate named export with updated properties (width, initialScale, themeColor).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • M4dhav

Poem

🐰 Hop, hop, skeleton screens so fine,
Shimmer, shimmer in a loading line,
No more blanks while content takes flight,
Just placeholders twinkling soft and bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a consistent skeleton loading UI system with shimmer animations applied across multiple key sections.
Linked Issues check ✅ Passed The PR successfully implements all primary coding objectives from issue #277: route-level loading UI, reusable skeleton components for common patterns, layout-mimicking placeholders, subtle shimmer animation, accessibility support, and dark mode support.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the skeleton loading implementation objectives. The modifications to app/layout.js viewport configuration and global CSS variables support the skeleton system and are not out-of-scope additions.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@shreyamittal239
Copy link
Author

Hi maintainers,

I have completed the UI/UX enhancement for consistent skeleton loading across the application.

This implementation introduces reusable skeleton components and route-level loading to eliminate layout shifts and blank regions during content loading. It improves perceived performance while maintaining accessibility, responsiveness, and dark mode support.

The build passes without warnings, and the changes have been tested locally.

Looking forward to your feedback. Happy to make any refinements if required.

Copy link

@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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/Hero/Hero.jsx (1)

1-14: ⚠️ Potential issue | 🔴 Critical

Missing "use client" directive in five components — this will throw a build error in Next.js App Router.

Hero.jsx, About.jsx, DownloadApp.jsx, Features.jsx, and TechStack.jsx all call useSkeletonLoading(), which internally uses useState and useEffect. By default, all components in the App Router (app/ directory) are Server Components unless marked otherwise. Using a React client hook in a Server Component is an error; you must mark each component using the hook as a Client Component by adding "use client" at the top of the file.

🔧 Fix required for all five files

Add this line to the top of each file, before any imports:

"use client";

Example for Hero.jsx:

+"use client";
+
 import "./Hero.css";
 import { FaGithub, FaArrowRight } from "react-icons/fa";

Apply the same change to About.jsx, DownloadApp.jsx, Features.jsx, and TechStack.jsx.

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

In `@app/components/Hero/Hero.jsx` around lines 1 - 14, The components Hero,
About, DownloadApp, Features, and TechStack call the client hook
useSkeletonLoading (which uses useState/useEffect) but are currently server
components; add the React Client Component directive by inserting "use client"
as the very first line of each file (before any imports) so Hero (component
Hero), About, DownloadApp, Features, and TechStack can legally call
useSkeletonLoading.
🧹 Nitpick comments (3)
app/components/Skeleton/index.js (1)

1-10: Consider exporting useSkeletonLoading from the barrel.

All consumers currently need two distinct import paths — ../Skeleton for components and ../Skeleton/useSkeletonLoading for the hook. Exporting the hook from index.js provides a single entry point consistent with a module barrel pattern.

♻️ Proposed addition
 export {
   SkeletonText,
   SkeletonTitle,
   SkeletonButton,
   SkeletonImage,
   SkeletonCircle,
   SkeletonCard,
   SkeletonRow,
   SkeletonIcon,
 } from "./Skeleton";
+export { useSkeletonLoading } from "./useSkeletonLoading";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Skeleton/index.js` around lines 1 - 10, Add the hook to the
barrel export so consumers can import everything from the same entry; update the
export in index.js to also re-export useSkeletonLoading (e.g., export {
useSkeletonLoading } from "./useSkeletonLoading" or export { default as
useSkeletonLoading } from "./useSkeletonLoading" depending on the hook's export
style) alongside the existing SkeletonText, SkeletonTitle, etc., ensuring the
symbol name useSkeletonLoading matches the hook file.
app/components/Skeleton/Skeleton.module.css (1)

31-39: Dead CSS: .skeleton-text, .skeleton-text:last-child, .skeleton-avatar, and .skeleton-circle are never applied.

  • SkeletonText applies only styles.skeleton per line, reproducing the height/margin/width logic entirely through inline styles — .skeleton-text and its :last-child variant are never used.
  • There is no SkeletonAvatar component, so .skeleton-avatar is unreachable.
  • SkeletonCircle uses inline styles for dimensions/radius, bypassing .skeleton-circle.

Either apply the CSS classes in the components (removing the inline duplication) or delete the unused rules.

Also applies to: 59-63, 80-84

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

In `@app/components/Skeleton/Skeleton.module.css` around lines 31 - 39, The CSS
rules for .skeleton-text (and its :last-child), .skeleton-avatar, and
.skeleton-circle are dead because the components use inline styles (SkeletonText
uses styles.skeleton per line and SkeletonCircle/SkeletonAvatar set dimensions
inline) or the component doesn't exist; update components to apply these CSS
classes instead of duplicating the styles inline or remove the unused CSS rules.
Specifically, in the SkeletonText component replace per-line inline
height/margin/width logic by applying styles.skeleton-text and relying on
.skeleton-text:last-child for the last-item rule (or remove the CSS and keep
inline styles), remove .skeleton-avatar if there is no SkeletonAvatar component
(or add the component and apply styles.skeleton-avatar), and either have
SkeletonCircle apply styles.skeleton-circle for radius/dimensions or delete the
.skeleton-circle rule so the stylesheet and components remain consistent.
app/components/Features/Features.jsx (1)

41-53: featureSkeletons is computed unconditionally on every render

The featureSkeletons array is built outside the if (isLoading) guard. After the 1.5s window, every re-render allocates and discards these nodes unnecessarily. Moving it inside the guard (or lazily computing it) avoids the waste.

♻️ Proposed refactor
-  const featureSkeletons = [1, 2, 3, 4].map((id) => (
-    <div
-      key={id}
-      className={`feature-item ${id % 2 === 0 ? "reverse" : ""}`}
-    >
-      <div className="feature-image">
-        <SkeletonImage aspectRatio="4/3" />
-      </div>
-      <div className="feature-content">
-        <SkeletonText lines={2} />
-      </div>
-    </div>
-  ));
-
   if (isLoading) {
-    return <section className="features">{featureSkeletons}</section>;
+    const featureSkeletons = [1, 2, 3, 4].map((id) => (
+      <div
+        key={id}
+        className={`feature-item ${id % 2 === 0 ? "reverse" : ""}`}
+      >
+        <div className="feature-image">
+          <SkeletonImage aspectRatio="4/3" />
+        </div>
+        <div className="feature-content">
+          <SkeletonText lines={2} />
+        </div>
+      </div>
+    ));
+    return <section className="features">{featureSkeletons}</section>;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Features/Features.jsx` around lines 41 - 53, The
featureSkeletons array is being allocated on every render even when not needed;
move its computation inside the isLoading conditional (or compute lazily) so
it’s only created when rendering the loading state. Update the component to only
build featureSkeletons within the block that checks isLoading (referencing
featureSkeletons and isLoading in Features.jsx), or replace the current
top-level map with a small helper function (e.g., renderFeatureSkeletons) and
call it only when isLoading is true so unnecessary allocations are avoided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/About/About.jsx`:
- Line 10: The hook useSkeletonLoading is forcing a 1.5s artificial loading
state for static components (About, TechStack, Features, DownloadApp) causing
unnecessary delay and SSR hydration flashes; update either the hook or its
usages so static pages opt out: change useSkeletonLoading to accept an optional
parameter (e.g., dataReady boolean or Promise or a duration) and return false
immediately when dataReady === true (or when no duration is provided), or simply
remove/replace the hook call in the listed components with isLoading = false for
static content; ensure the symbol useSkeletonLoading is updated and all
references in About, TechStack, Features, and DownloadApp are modified to pass a
readiness flag or bypass the timeout behavior to avoid client-side skeleton
flashes.
- Around line 22-27: The skeleton for the social links renders four
<SkeletonIcon /> elements but the real social-links list renders five anchors
(FaEnvelope, SiGitlab, FaGithub, FaDiscord, BsTwitterX), causing a layout shift;
update the social-links skeleton in About.jsx to render five placeholders
(either add one more <SkeletonIcon /> or iterate the same source array used for
the real links) so the skeleton structure matches the actual icons and prevents
the transition shift.
- Around line 12-31: The skeleton loading section returned when isLoading is
true lacks ARIA loading attributes; update the JSX in the About component's
isLoading branch (the <section className="about"> returned by the isLoading
check) to include aria-busy="true", role="status", and a descriptive aria-label
(e.g., "Loading about content") so screen-readers know this is a loading
placeholder; keep the existing visual skeleton elements unchanged.

In `@app/components/DownloadApp/DownloadApp.jsx`:
- Around line 3-7: This file is missing the React Server Components client
directive; add the `"use client"` pragma as the very first line of
DownloadApp.jsx (before any imports) so the DownloadApp component and its hook
useSkeletonLoading run on the client; ensure the directive is present and
mirrors the fix applied in About.jsx to avoid server/client mismatches for
DownloadApp, useSkeletonLoading, SkeletonTitle, SkeletonButton, SkeletonImage,
and SkeletonText.

In `@app/components/Features/Features.jsx`:
- Around line 6-7: This file defines the Features component and imports
client-only hooks/components (useSkeletonLoading, SkeletonImage, SkeletonText)
but is missing the React Server Components opt-in; add the "use client"
directive as the very first line of Features.jsx (before any imports) so the
Features component and its use of useSkeletonLoading/SkeletonImage/SkeletonText
run on the client; also apply the same "use client" addition to the related
component referenced at line 39 that uses these client-only symbols.

In `@app/components/Hero/Hero.jsx`:
- Around line 16-38: The skeleton <section className="hero"> rendered when
isLoading is true lacks ARIA context; update the JSX in the Hero component so
that when isLoading is true the <section> includes aria-busy="true" and a
descriptive aria-label (e.g., "Loading hero content"), and ensure the resolved
(non-loading) render clears aria-busy (set to "false" or remove the attribute)
so screen readers know loading has finished; locate the conditional branch that
checks isLoading in Hero.jsx to make this change.

In `@app/components/Skeleton/Skeleton.jsx`:
- Around line 3-88: The rendered skeleton divs are missing aria-hidden="true",
causing screen readers to announce meaningless regions; update each component to
add aria-hidden="true" on the root element: for SkeletonText and SkeletonRow add
aria-hidden="true" to their outer wrapper divs, and for SkeletonTitle,
SkeletonButton, SkeletonImage, SkeletonCircle, SkeletonCard, SkeletonIcon add
aria-hidden="true" to the single returned div in each function so all skeleton
elements are ignored by assistive tech.

In `@app/components/Skeleton/Skeleton.module.css`:
- Around line 92-97: Remove the duplicated dark-mode :root variable overrides in
Skeleton.module.css: delete the entire `@media` (prefers-color-scheme: dark) {
:root { --skeleton-base; --skeleton-highlight; --skeleton-card-bg; } } block so
the component relies on the single source of truth in globals.css for those
custom properties (--skeleton-base, --skeleton-highlight, --skeleton-card-bg).

In `@app/components/Skeleton/useSkeletonLoading.js`:
- Around line 5-16: The hook useSkeletonLoading currently always initializes
isLoading to true and relies only on a timer; change it to accept a loading
input (either a boolean or a Promise) and a minDelay (default 1500) so the hook
reflects real fetch state while still enforcing a minimum skeleton display.
Specifically: update useSkeletonLoading to take (loading, minDelay = 1500),
initialize isLoading from the provided loading boolean (or false if no boolean
provided) to avoid server-side skeletons, subscribe to a Promise if loading is a
Promise (set isLoading true until it resolves/rejects), and keep the setTimeout
logic only as a minimum-duration guard that prevents hiding the skeleton before
minDelay has elapsed; reference useSkeletonLoading, isLoading, setIsLoading, and
the internal timer/clearTimeout when implementing these changes.

In `@app/components/TechStack/TechStack.jsx`:
- Around line 4-8: The TechStack component is missing the client directive so
client-only hooks like useSkeletonLoading fail; add the "use client" directive
as the very first line of the TechStack.jsx file so the component (TechStack)
can call the useSkeletonLoading hook safely and remain a client component.

In `@app/loading.js`:
- Around line 3-9: The Loading component's spinner is decorative and the
container doesn't announce loading to assistive tech; update the JSX in Loading
to add aria-hidden="true" to the element with className styles.loadingSpinner,
and add role="status" and aria-live="polite" to the element with className
styles.loadingContainer (ensure the visible span with className
styles.loadingText remains for announcement or include an appropriate
aria-label/message inside the container).

---

Outside diff comments:
In `@app/components/Hero/Hero.jsx`:
- Around line 1-14: The components Hero, About, DownloadApp, Features, and
TechStack call the client hook useSkeletonLoading (which uses
useState/useEffect) but are currently server components; add the React Client
Component directive by inserting "use client" as the very first line of each
file (before any imports) so Hero (component Hero), About, DownloadApp,
Features, and TechStack can legally call useSkeletonLoading.

---

Nitpick comments:
In `@app/components/Features/Features.jsx`:
- Around line 41-53: The featureSkeletons array is being allocated on every
render even when not needed; move its computation inside the isLoading
conditional (or compute lazily) so it’s only created when rendering the loading
state. Update the component to only build featureSkeletons within the block that
checks isLoading (referencing featureSkeletons and isLoading in Features.jsx),
or replace the current top-level map with a small helper function (e.g.,
renderFeatureSkeletons) and call it only when isLoading is true so unnecessary
allocations are avoided.

In `@app/components/Skeleton/index.js`:
- Around line 1-10: Add the hook to the barrel export so consumers can import
everything from the same entry; update the export in index.js to also re-export
useSkeletonLoading (e.g., export { useSkeletonLoading } from
"./useSkeletonLoading" or export { default as useSkeletonLoading } from
"./useSkeletonLoading" depending on the hook's export style) alongside the
existing SkeletonText, SkeletonTitle, etc., ensuring the symbol name
useSkeletonLoading matches the hook file.

In `@app/components/Skeleton/Skeleton.module.css`:
- Around line 31-39: The CSS rules for .skeleton-text (and its :last-child),
.skeleton-avatar, and .skeleton-circle are dead because the components use
inline styles (SkeletonText uses styles.skeleton per line and
SkeletonCircle/SkeletonAvatar set dimensions inline) or the component doesn't
exist; update components to apply these CSS classes instead of duplicating the
styles inline or remove the unused CSS rules. Specifically, in the SkeletonText
component replace per-line inline height/margin/width logic by applying
styles.skeleton-text and relying on .skeleton-text:last-child for the last-item
rule (or remove the CSS and keep inline styles), remove .skeleton-avatar if
there is no SkeletonAvatar component (or add the component and apply
styles.skeleton-avatar), and either have SkeletonCircle apply
styles.skeleton-circle for radius/dimensions or delete the .skeleton-circle rule
so the stylesheet and components remain consistent.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b0fc48 and b6e0275.

📒 Files selected for processing (13)
  • app/components/About/About.jsx
  • app/components/DownloadApp/DownloadApp.jsx
  • app/components/Features/Features.jsx
  • app/components/Hero/Hero.jsx
  • app/components/Skeleton/Skeleton.jsx
  • app/components/Skeleton/Skeleton.module.css
  • app/components/Skeleton/index.js
  • app/components/Skeleton/useSkeletonLoading.js
  • app/components/TechStack/TechStack.jsx
  • app/globals.css
  • app/layout.js
  • app/loading.js
  • app/loading.module.css

import { SkeletonCircle, SkeletonTitle, SkeletonText, SkeletonIcon } from "../Skeleton";

const About = () => {
const isLoading = useSkeletonLoading();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timer-based fake loading unconditionally delays static content

useSkeletonLoading() always starts isLoading = true and flips it after a fixed 1500 ms setTimeout, regardless of whether content is actually being fetched. For these components (About, TechStack, Features, DownloadApp), all content is static — no async data fetch. This means:

  1. Every page load (and every re-mount on navigation) forces a 1.5s artificial wait, even when content is immediately available — worsening, not improving, perceived performance.
  2. On hard refresh with SSR, the server renders the loaded state but the client hydrates with isLoading = true, causing a brief skeleton flash that isn't driven by real resource loading.

If the intent is a "first-impression polish" delay, this trade-off should be documented. If it's meant to cover real async loading, the hook should instead accept a Promise or a data-ready signal.

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

In `@app/components/About/About.jsx` at line 10, The hook useSkeletonLoading is
forcing a 1.5s artificial loading state for static components (About, TechStack,
Features, DownloadApp) causing unnecessary delay and SSR hydration flashes;
update either the hook or its usages so static pages opt out: change
useSkeletonLoading to accept an optional parameter (e.g., dataReady boolean or
Promise or a duration) and return false immediately when dataReady === true (or
when no duration is provided), or simply remove/replace the hook call in the
listed components with isLoading = false for static content; ensure the symbol
useSkeletonLoading is updated and all references in About, TechStack, Features,
and DownloadApp are modified to pass a readiness flag or bypass the timeout
behavior to avoid client-side skeleton flashes.

Comment on lines +12 to +31
if (isLoading) {
return (
<section className="about">
<div className="about-container">
<div className="about-logo">
<SkeletonCircle size={120} />
</div>
<div className="about-content">
<SkeletonTitle style={{ width: "250px" }} />
<SkeletonText lines={4} />
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
</div>
</div>
</section>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skeleton section is missing ARIA loading state attributes

The PR lists screen-reader friendliness as an explicit goal, but the skeleton <section> has no aria-busy="true", no role="status", and no aria-label. Screen readers will attempt to navigate the empty skeleton <div> elements, providing no useful information.

🛠️ Proposed fix
-      <section className="about">
+      <section className="about" aria-busy="true" aria-label="Loading content">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isLoading) {
return (
<section className="about">
<div className="about-container">
<div className="about-logo">
<SkeletonCircle size={120} />
</div>
<div className="about-content">
<SkeletonTitle style={{ width: "250px" }} />
<SkeletonText lines={4} />
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
</div>
</div>
</section>
);
if (isLoading) {
return (
<section className="about" aria-busy="true" aria-label="Loading content">
<div className="about-container">
<div className="about-logo">
<SkeletonCircle size={120} />
</div>
<div className="about-content">
<SkeletonTitle style={{ width: "250px" }} />
<SkeletonText lines={4} />
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
</div>
</div>
</section>
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/About/About.jsx` around lines 12 - 31, The skeleton loading
section returned when isLoading is true lacks ARIA loading attributes; update
the JSX in the About component's isLoading branch (the <section
className="about"> returned by the isLoading check) to include aria-busy="true",
role="status", and a descriptive aria-label (e.g., "Loading about content") so
screen-readers know this is a loading placeholder; keep the existing visual
skeleton elements unchanged.

Comment on lines +22 to +27
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skeleton renders 4 icons; actual content has 5 social links

The actual social-links section (lines 52–68) renders 5 anchors (FaEnvelope, SiGitlab, FaGithub, FaDiscord, BsTwitterX), but the skeleton only renders 4 <SkeletonIcon /> elements. This causes a layout shift on transition — the very problem the PR aims to eliminate.

🛠️ Proposed fix
 <div className="social-links">
   <SkeletonIcon />
   <SkeletonIcon />
   <SkeletonIcon />
   <SkeletonIcon />
+  <SkeletonIcon />
 </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
<div className="social-links">
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
<SkeletonIcon />
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/About/About.jsx` around lines 22 - 27, The skeleton for the
social links renders four <SkeletonIcon /> elements but the real social-links
list renders five anchors (FaEnvelope, SiGitlab, FaGithub, FaDiscord,
BsTwitterX), causing a layout shift; update the social-links skeleton in
About.jsx to render five placeholders (either add one more <SkeletonIcon /> or
iterate the same source array used for the real links) so the skeleton structure
matches the actual icons and prevents the transition shift.

Comment on lines +3 to +7
import { useSkeletonLoading } from '../Skeleton/useSkeletonLoading';
import { SkeletonTitle, SkeletonButton, SkeletonImage, SkeletonText } from '../Skeleton';

const DownloadApp = () => {
const isLoading = useSkeletonLoading();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"use client" missing — same concern as About.jsx

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

In `@app/components/DownloadApp/DownloadApp.jsx` around lines 3 - 7, This file is
missing the React Server Components client directive; add the `"use client"`
pragma as the very first line of DownloadApp.jsx (before any imports) so the
DownloadApp component and its hook useSkeletonLoading run on the client; ensure
the directive is present and mirrors the fix applied in About.jsx to avoid
server/client mismatches for DownloadApp, useSkeletonLoading, SkeletonTitle,
SkeletonButton, SkeletonImage, and SkeletonText.

Comment on lines +6 to +7
import { useSkeletonLoading } from "../Skeleton/useSkeletonLoading";
import { SkeletonImage, SkeletonText } from "../Skeleton";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"use client" missing — same concern as About.jsx

Also applies to: 39-39

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

In `@app/components/Features/Features.jsx` around lines 6 - 7, This file defines
the Features component and imports client-only hooks/components
(useSkeletonLoading, SkeletonImage, SkeletonText) but is missing the React
Server Components opt-in; add the "use client" directive as the very first line
of Features.jsx (before any imports) so the Features component and its use of
useSkeletonLoading/SkeletonImage/SkeletonText run on the client; also apply the
same "use client" addition to the related component referenced at line 39 that
uses these client-only symbols.

Comment on lines +3 to +88
export function SkeletonText({ lines = 3, className = "", style = {} }) {
return (
<div className={className} style={style}>
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className={styles.skeleton}
style={{
height: "1em",
marginBottom: i < lines - 1 ? "0.5em" : "0",
width: i === lines - 1 ? "70%" : "100%",
}}
/>
))}
</div>
);
}

export function SkeletonTitle({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonTitle} ${className}`}
style={style}
/>
);
}

export function SkeletonButton({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonButton} ${className}`}
style={style}
/>
);
}

export function SkeletonImage({ aspectRatio = "4/3", className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonImage} ${className}`}
style={{ "--skeleton-aspect-ratio": aspectRatio, ...style }}
/>
);
}

export function SkeletonCircle({ size = 100, className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${className}`}
style={{
width: size,
height: size,
borderRadius: "50%",
...style,
}}
/>
);
}

export function SkeletonCard({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonCard} ${className}`}
style={style}
/>
);
}

export function SkeletonRow({ count = 3, className = "", style = {} }) {
return (
<div className={`${styles.skeletonRow} ${className}`} style={style}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className={styles.skeleton} style={{ height: "80px" }} />
))}
</div>
);
}

export function SkeletonIcon({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonIcon} ${className}`}
style={style}
/>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Skeleton placeholder elements are missing aria-hidden="true".

Every component renders empty <div> elements with no semantic meaning. Without aria-hidden="true", screen readers will tab into and announce these as meaningless anonymous regions. The PR objective explicitly requires screen-reader-friendly loaders.

Add aria-hidden="true" to each rendered element (or the outermost wrapper for SkeletonText and SkeletonRow):

♿ Representative diff for all eight components
 export function SkeletonText({ lines = 3, className = "", style = {} }) {
   return (
-    <div className={className} style={style}>
+    <div className={className} style={style} aria-hidden="true">
       {Array.from({ length: lines }).map((_, i) => (
 export function SkeletonTitle({ className = "", style = {} }) {
   return (
     <div
       className={`${styles.skeleton} ${styles.skeletonTitle} ${className}`}
+      aria-hidden="true"
       style={style}
     />

Apply the same pattern to SkeletonButton, SkeletonImage, SkeletonCircle, SkeletonCard, SkeletonRow, and SkeletonIcon.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function SkeletonText({ lines = 3, className = "", style = {} }) {
return (
<div className={className} style={style}>
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className={styles.skeleton}
style={{
height: "1em",
marginBottom: i < lines - 1 ? "0.5em" : "0",
width: i === lines - 1 ? "70%" : "100%",
}}
/>
))}
</div>
);
}
export function SkeletonTitle({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonTitle} ${className}`}
style={style}
/>
);
}
export function SkeletonButton({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonButton} ${className}`}
style={style}
/>
);
}
export function SkeletonImage({ aspectRatio = "4/3", className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonImage} ${className}`}
style={{ "--skeleton-aspect-ratio": aspectRatio, ...style }}
/>
);
}
export function SkeletonCircle({ size = 100, className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${className}`}
style={{
width: size,
height: size,
borderRadius: "50%",
...style,
}}
/>
);
}
export function SkeletonCard({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonCard} ${className}`}
style={style}
/>
);
}
export function SkeletonRow({ count = 3, className = "", style = {} }) {
return (
<div className={`${styles.skeletonRow} ${className}`} style={style}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className={styles.skeleton} style={{ height: "80px" }} />
))}
</div>
);
}
export function SkeletonIcon({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonIcon} ${className}`}
style={style}
/>
);
}
export function SkeletonText({ lines = 3, className = "", style = {} }) {
return (
<div className={className} style={style} aria-hidden="true">
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className={styles.skeleton}
style={{
height: "1em",
marginBottom: i < lines - 1 ? "0.5em" : "0",
width: i === lines - 1 ? "70%" : "100%",
}}
/>
))}
</div>
);
}
export function SkeletonTitle({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonTitle} ${className}`}
aria-hidden="true"
style={style}
/>
);
}
export function SkeletonButton({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonButton} ${className}`}
aria-hidden="true"
style={style}
/>
);
}
export function SkeletonImage({ aspectRatio = "4/3", className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonImage} ${className}`}
aria-hidden="true"
style={{ "--skeleton-aspect-ratio": aspectRatio, ...style }}
/>
);
}
export function SkeletonCircle({ size = 100, className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${className}`}
aria-hidden="true"
style={{
width: size,
height: size,
borderRadius: "50%",
...style,
}}
/>
);
}
export function SkeletonCard({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonCard} ${className}`}
aria-hidden="true"
style={style}
/>
);
}
export function SkeletonRow({ count = 3, className = "", style = {} }) {
return (
<div className={`${styles.skeletonRow} ${className}`} style={style} aria-hidden="true">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className={styles.skeleton} style={{ height: "80px" }} />
))}
</div>
);
}
export function SkeletonIcon({ className = "", style = {} }) {
return (
<div
className={`${styles.skeleton} ${styles.skeletonIcon} ${className}`}
aria-hidden="true"
style={style}
/>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Skeleton/Skeleton.jsx` around lines 3 - 88, The rendered
skeleton divs are missing aria-hidden="true", causing screen readers to announce
meaningless regions; update each component to add aria-hidden="true" on the root
element: for SkeletonText and SkeletonRow add aria-hidden="true" to their outer
wrapper divs, and for SkeletonTitle, SkeletonButton, SkeletonImage,
SkeletonCircle, SkeletonCard, SkeletonIcon add aria-hidden="true" to the single
returned div in each function so all skeleton elements are ignored by assistive
tech.

Comment on lines +92 to +97
@media (prefers-color-scheme: dark) {
:root {
--skeleton-base: #2a2a2a;
--skeleton-highlight: #3d3d3d;
--skeleton-card-bg: #1f1f1f;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove duplicate dark-mode :root overrides — already defined in globals.css.

These three custom properties are byte-for-byte identical to what's already in app/globals.css (lines 12-17). Because CSS custom properties on :root are always global — even inside a CSS module — this block is redundant and creates a two-source-of-truth problem: the values can drift when one file is updated without the other.

🗑️ Proposed removal
-.skeleton-icon { ... }
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --skeleton-base: `#2a2a2a`;
-    --skeleton-highlight: `#3d3d3d`;
-    --skeleton-card-bg: `#1f1f1f`;
-  }
-}
+.skeleton-icon { ... }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Skeleton/Skeleton.module.css` around lines 92 - 97, Remove the
duplicated dark-mode :root variable overrides in Skeleton.module.css: delete the
entire `@media` (prefers-color-scheme: dark) { :root { --skeleton-base;
--skeleton-highlight; --skeleton-card-bg; } } block so the component relies on
the single source of truth in globals.css for those custom properties
(--skeleton-base, --skeleton-highlight, --skeleton-card-bg).

Comment on lines +5 to +16
export function useSkeletonLoading(delay = 1500) {
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const timer = setTimeout(() => {
setIsLoading(false);
}, delay);

return () => clearTimeout(timer);
}, [delay]);

return isLoading;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Time-based simulation decoupled from actual data loading.

isLoading is driven solely by a wall-clock timer, not by any real async operation. This introduces two compounding problems:

  1. False timing: Content is hidden for a fixed 1500 ms even when it is already available, creating an artificial delay; conversely, if any data actually takes longer than 1500 ms to arrive, the skeleton dismisses before content is ready.
  2. SEO regression: In Next.js App Router, client components are pre-rendered on the server and hydrated on the client. Because useState(true) is the initial state, the server-rendered HTML for every consumer (Hero, Features, TechStack, etc.) will contain only empty skeleton <div> elements instead of real content — stripping the page of indexable text.

The hook should instead accept a Promise or a boolean loading prop that reflects actual data-fetching state, falling back to the timer only as a UX minimum:

-export function useSkeletonLoading(delay = 1500) {
-  const [isLoading, setIsLoading] = useState(true);
-
-  useEffect(() => {
-    const timer = setTimeout(() => {
-      setIsLoading(false);
-    }, delay);
-
-    return () => clearTimeout(timer);
-  }, [delay]);
-
-  return isLoading;
-}
+export function useSkeletonLoading(isDataLoading = false, minDelay = 0) {
+  const [timerDone, setTimerDone] = useState(minDelay === 0);
+
+  useEffect(() => {
+    if (minDelay === 0) return;
+    const timer = setTimeout(() => setTimerDone(true), minDelay);
+    return () => clearTimeout(timer);
+  }, [minDelay]);
+
+  return isDataLoading || !timerDone;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/Skeleton/useSkeletonLoading.js` around lines 5 - 16, The hook
useSkeletonLoading currently always initializes isLoading to true and relies
only on a timer; change it to accept a loading input (either a boolean or a
Promise) and a minDelay (default 1500) so the hook reflects real fetch state
while still enforcing a minimum skeleton display. Specifically: update
useSkeletonLoading to take (loading, minDelay = 1500), initialize isLoading from
the provided loading boolean (or false if no boolean provided) to avoid
server-side skeletons, subscribe to a Promise if loading is a Promise (set
isLoading true until it resolves/rejects), and keep the setTimeout logic only as
a minimum-duration guard that prevents hiding the skeleton before minDelay has
elapsed; reference useSkeletonLoading, isLoading, setIsLoading, and the internal
timer/clearTimeout when implementing these changes.

Comment on lines +4 to +8
import { useSkeletonLoading } from "../Skeleton/useSkeletonLoading";
import { SkeletonTitle, SkeletonImage } from "../Skeleton";

const TechStack = () => {
const isLoading = useSkeletonLoading();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

"use client" missing — same concern as About.jsx

useSkeletonLoading uses client-only hooks; this component needs "use client" for the same reason detailed in About.jsx.

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

In `@app/components/TechStack/TechStack.jsx` around lines 4 - 8, The TechStack
component is missing the client directive so client-only hooks like
useSkeletonLoading fail; add the "use client" directive as the very first line
of the TechStack.jsx file so the component (TechStack) can call the
useSkeletonLoading hook safely and remain a client component.

Comment on lines +3 to +9
export default function Loading() {
return (
<div className={styles.loadingContainer}>
<div className={styles.loadingSpinner}></div>
<span className={styles.loadingText}>Loading content...</span>
</div>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing ARIA attributes on loading UI.

The spinner div is purely decorative but has no aria-hidden="true", and the container lacks role="status" or aria-live="polite", so screen readers will neither announce the loading state properly nor suppress the meaningless spinner element.

♿ Proposed accessibility fix
 export default function Loading() {
   return (
-    <div className={styles.loadingContainer}>
-      <div className={styles.loadingSpinner}></div>
-      <span className={styles.loadingText}>Loading content...</span>
+    <div className={styles.loadingContainer} role="status" aria-live="polite">
+      <div className={styles.loadingSpinner} aria-hidden="true"></div>
+      <span className={styles.loadingText}>Loading content...</span>
     </div>
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function Loading() {
return (
<div className={styles.loadingContainer}>
<div className={styles.loadingSpinner}></div>
<span className={styles.loadingText}>Loading content...</span>
</div>
);
export default function Loading() {
return (
<div className={styles.loadingContainer} role="status" aria-live="polite">
<div className={styles.loadingSpinner} aria-hidden="true"></div>
<span className={styles.loadingText}>Loading content...</span>
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/loading.js` around lines 3 - 9, The Loading component's spinner is
decorative and the container doesn't announce loading to assistive tech; update
the JSX in Loading to add aria-hidden="true" to the element with className
styles.loadingSpinner, and add role="status" and aria-live="polite" to the
element with className styles.loadingContainer (ensure the visible span with
className styles.loadingText remains for announcement or include an appropriate
aria-label/message inside the container).

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.

UI/UX: ✨ Add skeleton screens and loading states for improved UX

1 participant