Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ba8eeb3
feat(image): add loading src to display loading state
Feb 2, 2025
33053b4
fix(image): fix sb issues
Feb 2, 2025
d763e24
test(image): add two cases (second test is not complete)
Feb 2, 2025
2eb1421
Merge branch 'heroui-inc:canary' into feat/image-loading-state_3640
Arian94 Feb 8, 2025
654dcf3
test(image): rectify the 2 tests regarding loading and fallback states
Arian94 Feb 8, 2025
de08d98
test(image): improve tests (cleanup renders)
Arian94 Feb 8, 2025
ef99401
test(image): add coderabbitai suggestions
Arian94 Feb 8, 2025
e48417c
Merge branch 'canary' into pr/4783
wingkwong Mar 2, 2025
b9dbaf0
Merge branch 'canary' into pr/4783
wingkwong Mar 19, 2025
1f37a3e
fix(docs): fix reported issues
Arian94 Jun 28, 2025
0146586
feat(image): add loadingImg and fallbackImg in classNames prop
Arian94 Jun 28, 2025
7da3d55
test(image): add tests for loadingImg and fallbackImg classNames
Arian94 Jun 28, 2025
96158c0
fix(use-image): merge wrapper styles with loading and fallback styles
Arian94 Jun 28, 2025
7856014
test(image.test): change tests for loading and fallback cases
Arian94 Jun 28, 2025
a409f6f
docs(image): update docs to include loading and fallback styles
Arian94 Jun 28, 2025
e192e93
fix(use-image): res conf
Arian94 Jun 29, 2025
62fdc74
fix: review issues
Arian94 Jul 4, 2025
2196ef0
docs: add changeset
Arian94 Jul 4, 2025
e3fbbda
docs: rectify changeset
Arian94 Jul 4, 2025
02252a1
fix: fix customLoading loading style - add classNames to mdx
Arian94 Jul 8, 2025
770bd53
fix(image.mdx): fix typo
Arian94 Jul 11, 2025
c9a3df6
fix(image.test): impl ai recom
Arian94 Jul 11, 2025
670b269
fix(image.test): fix ai-reported bug
Arian94 Jul 11, 2025
b0cf753
fix(image.test): fix func naming
Arian94 Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/docs/content/components/image/customLoading.raw.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Image} from "@heroui/react";

export default function App() {
return (
<Image
alt="HeroUI Image with custom loading"
height={200}
loadingSrc="https://via.placeholder.com/300x200"
src="https://app.requestly.io/delay/1000/https://nextui-docs-v2.vercel.app/images/fruit-4.jpeg"
width={300}
/>
);
}
9 changes: 9 additions & 0 deletions apps/docs/content/components/image/customLoading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import App from "./customLoading.raw.jsx?raw";

const react = {
"/App.jsx": App,
};

export default {
...react,
};
2 changes: 1 addition & 1 deletion apps/docs/content/components/image/fallback.raw.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default function App() {
alt="HeroUI Image with fallback"
fallbackSrc="https://via.placeholder.com/300x200"
height={200}
src="https://app.requestly.io/delay/1000/https://heroui.com/images/fruit-4.jpeg"
src="wrong-image-address"
width={300}
/>
);
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/content/components/image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import usage from "./usage";
import blurred from "./blurred";
import zoomed from "./zoomed";
import loading from "./loading";
import customLoading from "./customLoading";
import fallback from "./fallback";
import nextjs from "./nextjs";

Expand All @@ -10,6 +11,7 @@ export const imageContent = {
blurred,
zoomed,
loading,
customLoading,
fallback,
nextjs,
};
24 changes: 19 additions & 5 deletions apps/docs/content/docs/components/image.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,34 @@ You can use the `isZoomed` prop make the image zoomed when hovered.

<CodeDemo title="Zoomed" files={imageContent.zoomed} />

### Animated Loading
### Animated (Skeleton) Loading

Image component has a built-in `skeleton` animation to indicate the image is loading and an
`opacity` animation when the image loads.
Image component has a built-in `skeleton` animation to indicate the image is loading in case the `loadingSrc` is not defined.

<CodeDemo displayMode="visible" title="Animated Loading" files={imageContent.loading} />

> **Note**: The `URL` uses `https://app.requestly.io/delay` to simulate a slow network.

### Custom Loading Image

You can use the `loadingSrc` prop to display a loading image when the image provided in `src` is still loading.

<CodeDemo displayMode="visible" title="Custom Loading" files={imageContent.customLoading} />

> **Note**: The `URL` uses `https://app.requestly.io/delay` to simulate a slow network.

### Image with fallback

You can use the `fallbackSrc` prop to display a fallback image when:

- The `fallbackSrc` prop is provided.
- The image provided in `src` is still loading.
- The image provided in `src` fails to load.
- The image provided in `src` is not found.

<CodeDemo displayMode="visible" title="Image with fallback" files={imageContent.fallback} />

> **Note**: You can have both `loadingSrc` and `fallbackSrc` props to cover multiple possibilities while loading and handling image errors.

### With Next.js Image

Next.js provides an optimized [Image](https://nextjs.org/docs/app/api-reference/components/image) component,
Expand Down Expand Up @@ -152,11 +160,17 @@ you can use it with HeroUI `Image` component as well.
type: "eager | lazy",
description: "A loading strategy to use for the image.",
default: "-"
},
{
attribute: "loadingSrc",
type: "string",
description: "The image source to display while the main image is loading. This helps provide visual feedback during the loading process.",
default: "-"
},
{
attribute: "fallbackSrc",
type: "string",
description: "The fallback image source.",
description: "The image source to display when the main image fails to load or encounters an error.",
default: "-"
},
{
Expand Down
3 changes: 2 additions & 1 deletion packages/components/image/src/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Image = forwardRef<"img", ImageProps>((props, ref) => {
classNames,
isBlurred,
isZoomed,
loadingSrc,
fallbackSrc,
removeWrapper,
disableSkeleton,
Expand Down Expand Up @@ -45,7 +46,7 @@ const Image = forwardRef<"img", ImageProps>((props, ref) => {
}

// when zoomed or showSkeleton, we need to wrap the image
if (isZoomed || !disableSkeleton || fallbackSrc) {
if (isZoomed || !disableSkeleton || loadingSrc || fallbackSrc) {
return <div {...getWrapperProps()}> {isZoomed ? zoomed : img}</div>;
}

Expand Down
26 changes: 19 additions & 7 deletions packages/components/image/src/use-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ interface Props extends HTMLHeroUIProps<"img"> {
*/
isBlurred?: boolean;
/**
* A fallback image.
* A fallback image when error encountered.
*/
fallbackSrc?: React.ReactNode;
/**
* A loading image.
*/
loadingSrc?: React.ReactNode;
/**
* Whether to disable the loading skeleton.
* @default false
Expand Down Expand Up @@ -82,9 +86,10 @@ export function useImage(originalProps: UseImageProps) {
classNames,
loading,
isBlurred,
loadingSrc,
fallbackSrc,
isLoading: isLoadingProp,
disableSkeleton = !!fallbackSrc,
disableSkeleton = !!loadingSrc,
removeWrapper = false,
onError,
onLoad,
Expand All @@ -110,6 +115,7 @@ export function useImage(originalProps: UseImageProps) {

const isImgLoaded = imageStatus === "loaded" && !isLoadingProp;
const isLoading = imageStatus === "loading" || isLoadingProp;
const isFailed = imageStatus === "failed";
const isZoomed = originalProps.isZoomed;

const Component = as || "img";
Expand All @@ -131,8 +137,9 @@ export function useImage(originalProps: UseImageProps) {
};
}, [props?.width, props?.height]);

const showFallback = (!src || !isImgLoaded) && !!fallbackSrc;
const showSkeleton = isLoading && !disableSkeleton;
const showLoading = isLoading && !!loadingSrc;
const showFallback = (isFailed || !src || !isImgLoaded) && !!fallbackSrc;
const showSkeleton = isLoading && !disableSkeleton && !loadingSrc;

const slots = useMemo(
() =>
Expand Down Expand Up @@ -170,7 +177,11 @@ export function useImage(originalProps: UseImageProps) {
};

const getWrapperProps = useCallback<PropGetter>(() => {
const fallbackStyle = showFallback
const wrapperStyle = showLoading
? {
backgroundImage: `url(${loadingSrc})`,
}
: showFallback && !showSkeleton
? {
backgroundImage: `url(${fallbackSrc})`,
}
Expand All @@ -179,11 +190,11 @@ export function useImage(originalProps: UseImageProps) {
return {
className: slots.wrapper({class: classNames?.wrapper}),
style: {
...fallbackStyle,
...wrapperStyle,
maxWidth: w,
},
};
}, [slots, showFallback, fallbackSrc, classNames?.wrapper, w]);
}, [slots, showLoading, showFallback, showSkeleton, fallbackSrc, classNames?.wrapper, w]);

const getBlurredImgProps = useCallback<PropGetter>(() => {
return {
Expand All @@ -200,6 +211,7 @@ export function useImage(originalProps: UseImageProps) {
classNames,
isBlurred,
disableSkeleton,
loadingSrc,
fallbackSrc,
removeWrapper,
isZoomed,
Expand Down
36 changes: 32 additions & 4 deletions packages/components/image/stories/image.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,19 @@ export const Shadow = {
},
};

export const Skeleton = {
render: LoadingTemplate,

args: {
...defaultProps,
width: 300,
height: 450,
radius: "lg",
src: "https://app.requestly.io/delay/3000/https://images.unsplash.com/photo-1494790108377-be9c29b29330",
disableSkeleton: false,
},
};

export const AnimatedLoad = {
args: {
...defaultProps,
Expand All @@ -123,27 +136,42 @@ export const AnimatedLoad = {
},
};

export const Fallback = {
export const CustomLoading = {
render: LoadingTemplate,

args: {
...defaultProps,
width: 300,
radius: "lg",
src: "https://app.requestly.io/delay/3000/https://images.unsplash.com/photo-1539571696357-5a69c17a67c6",
fallbackSrc: "/images/placeholder_300x450.png",
loadingSrc: "/images/placeholder_300x450.png",
},
};

export const Skeleton = {
export const Fallback = {
render: LoadingTemplate,

args: {
...defaultProps,
width: 300,
height: 450,
radius: "lg",
src: "https://app.requestly.io/delay/3000/https://images.unsplash.com/photo-1494790108377-be9c29b29330",
src: "wrong-src-address",
fallbackSrc: "/images/placeholder_300x450.png",
disableSkeleton: false,
},
};

export const CustomLoadingAndFallback = {
render: LoadingTemplate,

args: {
...defaultProps,
width: 300,
height: 450,
radius: "lg",
src: "wrong-src-address",
loadingSrc: "/images/placeholder_300x450.png",
fallbackSrc: "/images/local-image-small.jpg",
},
};