Tags
This is a sample implementation of an advanced image component that works with Caisy assets. The component demonstrates how to build a sophisticated image renderer with focal point support, lazy loading, responsive image sizing, and smooth loading transitions with placeholder effects.
This sample implementation includes:
Responsive Images: Demonstrates automatic generation of multiple image sizes for different breakpoints
Lazy Loading: Shows how to implement optional lazy loading with configurable modes
Placeholder Effects: Example of BlurHash and dominant color placeholder integration
Focal Point: Sample focal point support for optimal image cropping
Smooth Transitions: Implementation of fade-in animations during image loading
SVG Support: Example of special SVG image handling
Accessibility: Demonstrates proper alt text and title attribute usage
interface IFancyImage {
img: IGenAsset;
responsiveFactor?: IResponseOptionalValue<number>;
responsiveAspectRatio?: IResponseOptionalValue<number>;
responsiveMaxWidth?: IResponseOptionalValue<number>;
lazyload?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
onLoad?: ReactEventHandler<HTMLImageElement>;
inspectProps?: any;
}
img
(required): The image asset object containing src, dimensions, and metadata
responsiveFactor
: Controls how much screen space the image occupies (0-1), defaults to 1 (full width)
responsiveAspectRatio
: Override default aspect ratio for each breakpoint
responsiveMaxWidth
: Maximum width in pixels for each breakpoint
lazyload
: Enable/disable lazy loading (default: true)
onClick
: Click handler for the image container
onLoad
: Callback when image finishes loading
// Image asset interface
export type IGenAsset = {
__typename: 'Asset';
_meta?: Maybe<IGenCaisyDocument_Meta>;
author?: Maybe<string>;
blurHash?: Maybe<string>;
copyright?: Maybe<string>;
description?: Maybe<string>;
dominantColor?: Maybe<string>;
height?: Maybe<number>;
id?: Maybe<string>;
keywords?: Maybe<string>;
originalName?: Maybe<string>;
originType?: Maybe<string>;
src?: Maybe<string>;
subtitle?: Maybe<string>;
title?: Maybe<string>;
width?: Maybe<number>;
};
// Responsive value interface for breakpoint-specific values
export interface IResponseOptionalValue<T> {
bronze?: T;
silver?: T;
gold?: T;
platinum?: T;
diamond?: T;
master?: T;
}
import { FC, MouseEventHandler, ReactEventHandler, useCallback, useEffect, useRef, useState } from "react";
import styled, { css, keyframes } from "styled-components";
import { blurhashToCssGradient } from "blurhash-to-css-gradient";
// Constants for breakpoints (example values)
const BREAKPOINTS = {
SILVER: 768,
GOLD: 1024,
PLATINUM: 1440,
DIAMOND: 1920,
MASTER: 2560,
CHALLENGER: 3840
};
// Media query helpers
const MIN_SILVER = (styles: any) => css`@media (min-width: ${BREAKPOINTS.SILVER}px) { ${styles} }`;
const MIN_GOLD = (styles: any) => css`@media (min-width: ${BREAKPOINTS.GOLD}px) { ${styles} }`;
const MIN_PLATINUM = (styles: any) => css`@media (min-width: ${BREAKPOINTS.PLATINUM}px) { ${styles} }`;
const MIN_DIAMOND = (styles: any) => css`@media (min-width: ${BREAKPOINTS.DIAMOND}px) { ${styles} }`;
const MIN_MASTER = (styles: any) => css`@media (min-width: ${BREAKPOINTS.MASTER}px) { ${styles} }`;
// Types
export type IGenAsset = {
__typename: 'Asset';
author?: string;
blurHash?: string;
copyright?: string;
description?: string;
dominantColor?: string;
height?: number;
id?: string;
keywords?: string;
originalName?: string;
originType?: string;
src?: string;
subtitle?: string;
title?: string;
width?: number;
};
export interface IResponseOptionalValue<T> {
bronze?: T;
silver?: T;
gold?: T;
platinum?: T;
diamond?: T;
master?: T;
}
export interface IFancyImage {
img: IGenAsset;
responsiveFactor?: IResponseOptionalValue<number>;
responsiveAspectRatio?: IResponseOptionalValue<number>;
responsiveMaxWidth?: IResponseOptionalValue<number>;
lazyload?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
onLoad?: ReactEventHandler<HTMLImageElement>;
inspectProps?: any;
}
// Styled Components
// Skeleton/Placeholder with pulse animation
const pulseAnimation = keyframes`
0% { opacity: 0.8; }
50% { opacity: 1; }
100% { opacity: 0.8; }
`;
const SFancyImageSkeleton = styled.div<{ background: string }>`
width: 100%;
height: 100%;
background: ${({ background }) => background};
animation: ${pulseAnimation} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
`;
// Image with fade-in transition
const SFancyImageImg = styled.img<{ isLoaded?: boolean }>`
transition: opacity 0.3s ease;
opacity: ${(props) => (props.isLoaded ? 1 : 0)};
object-fit: cover;
height: 100%;
width: 100%;
display: block;
`;
// Aspect ratio container
interface ISFancyImageAspectContainerProps {
brozeAspectRatio: number;
silverAspectRatio: number;
goldAspectRatio: number;
platinumAspectRatio: number;
diamondAspectRatio: number;
masterAspectRatio: number;
hasLoaded: boolean;
}
const SFancyImageAspectContainer = styled.div<ISFancyImageAspectContainerProps>`
position: relative;
z-index: 0;
width: 100%;
> .absolute-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
> .layer-z1 {
z-index: 1;
transition: opacity 0.3s ease-in-out;
${({ hasLoaded }) => hasLoaded && css`opacity: 0;`}
}
> .layer-z2 {
z-index: 2;
}
:before {
pointer-events: none;
content: "";
width: 100%;
position: relative;
display: block;
padding-bottom: ${(props) => (1 / props.brozeAspectRatio) * 100}%;
}
${MIN_SILVER(css`
:before {
padding-bottom: ${(props: ISFancyImageAspectContainerProps) => (1 / props.silverAspectRatio) * 100}%;
}
`)};
${MIN_GOLD(css`
:before {
padding-bottom: ${(props: ISFancyImageAspectContainerProps) => (1 / props.goldAspectRatio) * 100}%;
}
`)};
${MIN_PLATINUM(css`
:before {
padding-bottom: ${(props: ISFancyImageAspectContainerProps) => (1 / props.platinumAspectRatio) * 100}%;
}
`)};
${MIN_DIAMOND(css`
:before {
padding-bottom: ${(props: ISFancyImageAspectContainerProps) => (1 / props.diamondAspectRatio) * 100}%;
}
`)};
${MIN_MASTER(css`
:before {
padding-bottom: ${(props: ISFancyImageAspectContainerProps) => (1 / props.masterAspectRatio) * 100}%;
}
`)};
`;
// Utility function to calculate responsive image sizes
const calculateResponsiveImagesSizes = ({
responsiveFactor,
responsiveMaxWidth,
responsiveAspectRatio,
images,
}: {
responsiveFactor?: IResponseOptionalValue<number>;
responsiveMaxWidth?: IResponseOptionalValue<number>;
responsiveAspectRatio?: IResponseOptionalValue<number>;
images: IGenAsset[];
}) => {
const img = images[0];
const defaultAspectRatio = (img.width || 1) / (img.height || 1);
const calculateSize = (breakpoint: keyof IResponseOptionalValue<number>, breakpointWidth: number) => {
const factor = responsiveFactor?.[breakpoint] || 1;
const maxWidth = responsiveMaxWidth?.[breakpoint] || breakpointWidth;
const aspectRatio = responsiveAspectRatio?.[breakpoint] || defaultAspectRatio;
const width = Math.min(breakpointWidth * factor, maxWidth);
const height = width / aspectRatio;
return { width: Math.round(width), height: Math.round(height) };
};
return {
bronze: calculateSize('bronze', 480),
silver: calculateSize('silver', BREAKPOINTS.SILVER),
gold: calculateSize('gold', BREAKPOINTS.GOLD),
platinum: calculateSize('platinum', BREAKPOINTS.PLATINUM),
diamond: calculateSize('diamond', BREAKPOINTS.DIAMOND),
master: calculateSize('master', BREAKPOINTS.MASTER),
};
};
// Main Component
export const CaisyImage: FC<React.PropsWithChildren<ICaisyImage>> = ({
img,
responsiveFactor,
responsiveAspectRatio,
responsiveMaxWidth,
lazyload = true,
onClick,
onLoad,
}) => {
const loading = lazyload ? "lazy" : "eager";
const [isImgLoaded, setIsImgLoaded] = useState(false);
const imageRef = useRef<HTMLImageElement>(null);
const handleOnLoad = useCallback(() => {
setIsImgLoaded(true);
}, [setIsImgLoaded]);
useEffect(() => {
if (!imageRef.current) {
return;
}
if (imageRef.current.complete) {
setIsImgLoaded(true);
return;
}
imageRef.current.onload = handleOnLoad;
}, [imageRef.current, handleOnLoad]);
if (!img || !img.src) {
return null;
}
const sizes = calculateResponsiveImagesSizes({
responsiveFactor,
responsiveMaxWidth,
responsiveAspectRatio,
images: [img],
});
const actualSrc: string = img.src;
let _src = actualSrc;
const hasSVG = _src.includes(".svg");
if (!hasSVG) {
_src = _src.includes("?") ? _src + "&" : _src + "?";
}
const srcSet = `${_src + `width=${sizes.bronze.width}&height=${sizes.bronze.height}`} ${BREAKPOINTS.SILVER - 1}w,
${_src + `width=${sizes.silver.width}&height=${sizes.silver.height}`} ${BREAKPOINTS.GOLD - 1}w,
${_src + `width=${sizes.gold.width}&height=${sizes.gold.height}`} ${BREAKPOINTS.PLATINUM - 1}w,
${_src + `width=${sizes.platinum.width}&height=${sizes.platinum.height}`} ${BREAKPOINTS.DIAMOND - 1}w,
${_src + `width=${sizes.diamond.width}&height=${sizes.diamond.height}`} ${BREAKPOINTS.MASTER - 1}w,
${_src + `width=${sizes.master.width}&height=${sizes.master.height}`} ${BREAKPOINTS.CHALLENGER - 1}w`;
let placeholderBackground = "#e3e3e3e3";
const { dominantColor, blurHash } = img;
if (blurHash) {
try {
placeholderBackground = blurhashToCssGradient(blurHash);
} catch {}
} else {
if (dominantColor) {
if (dominantColor.startsWith("#")) {
placeholderBackground = dominantColor;
} else {
placeholderBackground = `#${dominantColor}`;
}
}
}
return (
<SFancyImageAspectContainer
brozeAspectRatio={sizes.bronze.width / sizes.bronze.height}
silverAspectRatio={sizes.silver.width / sizes.silver.height}
goldAspectRatio={sizes.gold.width / sizes.gold.height}
platinumAspectRatio={sizes.platinum.width / sizes.platinum.height}
diamondAspectRatio={sizes.diamond.width / sizes.diamond.height}
masterAspectRatio={sizes.master.width / sizes.master.height}
onClick={onClick}
hasLoaded={isImgLoaded}
>
<div className="absolute-layer layer-z1">
<SFancyImageSkeleton background={placeholderBackground} />
</div>
<div className="absolute-layer layer-z2">
<SFancyImageImg
ref={imageRef}
src={hasSVG ? img.src : `${_src}width=${sizes.bronze.width}&height=${sizes.bronze.height}`}
isLoaded={isImgLoaded}
srcSet={!hasSVG ? srcSet : ""}
sizes="100vw"
title={img.description || undefined}
alt={img.description || undefined}
loading={loading}
onLoad={(e) => {
handleOnLoad();
typeof onLoad === "function" && onLoad(e);
}}
/>
</div>
</SFancyImageAspectContainer>
);
};
<CaisyImage img={imageAsset} />
<CaisyImage
img={imageAsset}
responsiveFactor={{
bronze: 1, // Full width on mobile
silver: 0.5, // Half width on tablet
gold: 0.33 // Third width on desktop
}}
responsiveMaxWidth={{
bronze: 480,
silver: 400,
gold: 300
}}
/>
<CaisyImage
img={imageAsset}
lazyload={false}
onClick={() => console.log('Image clicked')}
onLoad={() => console.log('Image loaded')}
/>
This sample demonstrates several advanced techniques:
Shows how to generate responsive srcSet
attributes for optimal performance across devices
Demonstrates BlurHash to CSS gradient conversion for seamless loading experiences
Illustrates special SVG handling without responsive transformations
Examples of layered architecture with absolute positioning for smooth transitions
Shows aspect ratio maintenance across breakpoints using CSS padding-bottom technique
Demonstrates integration patterns with Caisy CMS asset structure
react
- Core React functionality
styled-components
- CSS-in-JS styling
blurhash-to-css-gradient
- BlurHash to CSS gradient conversion
Subscribe to our newsletters
and stay updated
While you subscribe you agree to our Privacy Policy and Terms of Service apply.
Tags