Network Image
Image component that adapts quality and loading strategy in real-time based on connection speed using the Network Information API.
Component

4G
"use client";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
interface NetworkInformation {
effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
downlink?: number;
saveData?: boolean;
addEventListener(
type: "change",
listener: () => void,
): void;
removeEventListener(
type: "change",
listener: () => void,
): void;
}
declare global {
interface Navigator {
connection?: NetworkInformation;
}
}
export type EffectiveType =
| "slow-2g"
| "2g"
| "3g"
| "4g"
| "unknown";
export interface NetworkStatus {
effectiveType: EffectiveType;
downlink: number;
saveData: boolean;
isOnline: boolean;
}
function useNetworkStatus(): NetworkStatus {
const [status, setStatus] = useState<NetworkStatus>(
() => {
if (typeof window === "undefined") {
return {
downlink: 10,
effectiveType: "4g",
isOnline: true,
saveData: false,
};
}
const conn = navigator.connection;
const isOnline = navigator.onLine;
if (!conn) {
return {
downlink: 10,
effectiveType: "4g",
isOnline,
saveData: false,
};
}
return {
downlink: conn.downlink ?? 10,
effectiveType: (conn.effectiveType ??
"4g") as EffectiveType,
isOnline,
saveData: conn.saveData ?? false,
};
},
);
const update = useCallback(() => {
if (typeof window === "undefined") {
return;
}
const conn = navigator.connection;
const isOnline = navigator.onLine;
if (!conn) {
setStatus({
downlink: 10,
effectiveType: "4g",
isOnline,
saveData: false,
});
return;
}
setStatus({
downlink: conn.downlink ?? 10,
effectiveType: (conn.effectiveType ??
"4g") as EffectiveType,
isOnline,
saveData: conn.saveData ?? false,
});
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
window.addEventListener("online", update);
window.addEventListener("offline", update);
const conn = navigator.connection;
if (conn) {
conn.addEventListener("change", update);
}
return () => {
window.removeEventListener("online", update);
window.removeEventListener("offline", update);
if (conn) {
conn.removeEventListener("change", update);
}
};
}, [update]);
return status;
}
type ImageTier = "full" | "medium" | "low";
function getImageTier(status: NetworkStatus): ImageTier {
if (!status.isOnline) {
return "low";
}
if (status.saveData) {
return "low";
}
switch (status.effectiveType) {
case "4g":
return "full";
case "3g":
return "medium";
default:
return "low";
}
}
function getLoadingStrategy(
status: NetworkStatus,
): "eager" | "lazy" {
if (!status.isOnline) {
return "lazy";
}
if (status.saveData) {
return "lazy";
}
return status.effectiveType === "4g" ? "eager" : "lazy";
}
function getBadgeLabel(status: NetworkStatus): string {
if (!status.isOnline) {
return "Offline";
}
if (status.saveData) {
return "Data Saver";
}
switch (status.effectiveType) {
case "4g":
return "4G";
case "3g":
return "3G";
case "2g":
case "slow-2g":
return "2G";
default:
return "4G";
}
}
interface NetworkImageProps {
src: string;
alt: string;
lowSrc?: string;
mediumSrc?: string;
width?: number;
height?: number;
className?: string;
showBadge?: boolean;
loadingStrategy?: "auto" | "eager" | "lazy";
networkOverride?: Partial<NetworkStatus>;
}
export function NetworkImage({
src,
alt,
lowSrc,
mediumSrc,
width,
height,
className,
showBadge = false,
loadingStrategy = "auto",
networkOverride,
}: NetworkImageProps) {
const realStatus = useNetworkStatus();
const status: NetworkStatus = networkOverride
? { ...realStatus, ...networkOverride }
: realStatus;
const tier = getImageTier(status);
const loading =
loadingStrategy === "auto"
? getLoadingStrategy(status)
: loadingStrategy;
const imageSrc = (() => {
switch (tier) {
case "full":
return src;
case "medium":
return mediumSrc ?? src;
case "low":
return lowSrc ?? mediumSrc ?? src;
}
})();
const isOffline = !status.isOnline;
return (
<div
className={cn(
"relative overflow-hidden rounded-lg",
className,
)}
>
{isOffline ? (
<div
className="flex items-center justify-center bg-muted text-muted-foreground"
style={{
height: height ?? 200,
minHeight: 120,
width: width ?? "100%",
}}
>
<div className="flex flex-col items-center gap-2 text-center">
<svg
aria-hidden={true}
className="size-8 opacity-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3"
/>
</svg>
<span className="font-medium text-sm">
Offline
</span>
</div>
</div>
) : (
<img
alt={alt}
className="h-auto w-full object-cover transition-opacity duration-300"
height={height}
loading={loading}
src={imageSrc}
width={width}
/>
)}
{showBadge && (
<div
className={cn(
"absolute top-2 right-2 rounded-md px-2 py-0.5 font-medium text-xs",
"bg-background/80 text-foreground backdrop-blur-sm",
isOffline &&
"bg-destructive/80 text-destructive-foreground",
)}
>
{getBadgeLabel(status)}
</div>
)}
</div>
);
}Installation
pnpm dlx shadcn@latest add "https://pulkitxm.com/components/network-image.json"1. Copy the component file
"use client";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
interface NetworkInformation {
effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
downlink?: number;
saveData?: boolean;
addEventListener(
type: "change",
listener: () => void,
): void;
removeEventListener(
type: "change",
listener: () => void,
): void;
}
declare global {
interface Navigator {
connection?: NetworkInformation;
}
}
export type EffectiveType =
| "slow-2g"
| "2g"
| "3g"
| "4g"
| "unknown";
export interface NetworkStatus {
effectiveType: EffectiveType;
downlink: number;
saveData: boolean;
isOnline: boolean;
}
function useNetworkStatus(): NetworkStatus {
const [status, setStatus] = useState<NetworkStatus>(
() => {
if (typeof window === "undefined") {
return {
downlink: 10,
effectiveType: "4g",
isOnline: true,
saveData: false,
};
}
const conn = navigator.connection;
const isOnline = navigator.onLine;
if (!conn) {
return {
downlink: 10,
effectiveType: "4g",
isOnline,
saveData: false,
};
}
return {
downlink: conn.downlink ?? 10,
effectiveType: (conn.effectiveType ??
"4g") as EffectiveType,
isOnline,
saveData: conn.saveData ?? false,
};
},
);
const update = useCallback(() => {
if (typeof window === "undefined") {
return;
}
const conn = navigator.connection;
const isOnline = navigator.onLine;
if (!conn) {
setStatus({
downlink: 10,
effectiveType: "4g",
isOnline,
saveData: false,
});
return;
}
setStatus({
downlink: conn.downlink ?? 10,
effectiveType: (conn.effectiveType ??
"4g") as EffectiveType,
isOnline,
saveData: conn.saveData ?? false,
});
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
window.addEventListener("online", update);
window.addEventListener("offline", update);
const conn = navigator.connection;
if (conn) {
conn.addEventListener("change", update);
}
return () => {
window.removeEventListener("online", update);
window.removeEventListener("offline", update);
if (conn) {
conn.removeEventListener("change", update);
}
};
}, [update]);
return status;
}
type ImageTier = "full" | "medium" | "low";
function getImageTier(status: NetworkStatus): ImageTier {
if (!status.isOnline) {
return "low";
}
if (status.saveData) {
return "low";
}
switch (status.effectiveType) {
case "4g":
return "full";
case "3g":
return "medium";
default:
return "low";
}
}
function getLoadingStrategy(
status: NetworkStatus,
): "eager" | "lazy" {
if (!status.isOnline) {
return "lazy";
}
if (status.saveData) {
return "lazy";
}
return status.effectiveType === "4g" ? "eager" : "lazy";
}
function getBadgeLabel(status: NetworkStatus): string {
if (!status.isOnline) {
return "Offline";
}
if (status.saveData) {
return "Data Saver";
}
switch (status.effectiveType) {
case "4g":
return "4G";
case "3g":
return "3G";
case "2g":
case "slow-2g":
return "2G";
default:
return "4G";
}
}
interface NetworkImageProps {
src: string;
alt: string;
lowSrc?: string;
mediumSrc?: string;
width?: number;
height?: number;
className?: string;
showBadge?: boolean;
loadingStrategy?: "auto" | "eager" | "lazy";
networkOverride?: Partial<NetworkStatus>;
}
export function NetworkImage({
src,
alt,
lowSrc,
mediumSrc,
width,
height,
className,
showBadge = false,
loadingStrategy = "auto",
networkOverride,
}: NetworkImageProps) {
const realStatus = useNetworkStatus();
const status: NetworkStatus = networkOverride
? { ...realStatus, ...networkOverride }
: realStatus;
const tier = getImageTier(status);
const loading =
loadingStrategy === "auto"
? getLoadingStrategy(status)
: loadingStrategy;
const imageSrc = (() => {
switch (tier) {
case "full":
return src;
case "medium":
return mediumSrc ?? src;
case "low":
return lowSrc ?? mediumSrc ?? src;
}
})();
const isOffline = !status.isOnline;
return (
<div
className={cn(
"relative overflow-hidden rounded-lg",
className,
)}
>
{isOffline ? (
<div
className="flex items-center justify-center bg-muted text-muted-foreground"
style={{
height: height ?? 200,
minHeight: 120,
width: width ?? "100%",
}}
>
<div className="flex flex-col items-center gap-2 text-center">
<svg
aria-hidden={true}
className="size-8 opacity-50"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3"
/>
</svg>
<span className="font-medium text-sm">
Offline
</span>
</div>
</div>
) : (
<img
alt={alt}
className="h-auto w-full object-cover transition-opacity duration-300"
height={height}
loading={loading}
src={imageSrc}
width={width}
/>
)}
{showBadge && (
<div
className={cn(
"absolute top-2 right-2 rounded-md px-2 py-0.5 font-medium text-xs",
"bg-background/80 text-foreground backdrop-blur-sm",
isOffline &&
"bg-destructive/80 text-destructive-foreground",
)}
>
{getBadgeLabel(status)}
</div>
)}
</div>
);
}2. Import and use
import { NetworkImage } from "@/components/network-image";
<NetworkImage
src="https://example.com/image.jpg"
alt="Description"
/>;Usage
Import the component
Add the NetworkImage import to your file.
import { NetworkImage } from "@/components/network-image";Use with default props
Use with required src and alt. The component adapts automatically.
<NetworkImage
src="https://example.com/image.jpg"
alt="Description"
/>;Customize with props
Provide quality tiers and show the connection badge for debugging.
<NetworkImage
src="/full.jpg"
mediumSrc="/medium.jpg"
lowSrc="/thumb.jpg"
alt="Adaptive"
showBadge
/>;Guidelines
- Provide lowSrc and mediumSrc for best results on slow connections; otherwise the full src is used for all tiers.
- Use showBadge during development to verify the component detects your connection correctly.
- The Network Information API has limited browser support (Chromium); the component falls back to 4g behavior when unavailable.
- Respect saveData: when the user has enabled reduced data usage, the component uses the low tier.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| src | string | — | Primary image URL. Used for 4g connections. |
| alt | string | — | Accessible alt text for the image. |
| lowSrc | string | undefined | Low-resolution URL for 2g/slow-2g. Falls back to mediumSrc or src if not provided. |
| mediumSrc | string | undefined | Medium-resolution URL for 3g. Falls back to src if not provided. |
| width | number | undefined | Image width in pixels. |
| height | number | undefined | Image height in pixels. |
| className | string | undefined | Additional CSS classes for the wrapper. |
| showBadge | boolean | false | Show a badge overlay with the detected connection type (4G, 3G, 2G, Offline). |
| loadingStrategy | "auto" | "eager" | "lazy" | "auto" | Loading strategy. 'auto' adapts by network; 'eager' or 'lazy' override. |
| networkOverride | Partial<NetworkStatus> | undefined | Override network status for testing/demo. Not for production use. |
Examples
Basic
Default usage. Uses full resolution on fast connections, adapts on slower ones.

4G
import { NetworkImage } from "@/components/network-image";
export function NetworkImageBasic() {
return (
<NetworkImage
src="https://picsum.photos/800/400"
alt="Adaptive image"
/>
);
}Quality tiers
Provide different image URLs for each connection tier.

4G
import { NetworkImage } from "@/components/network-image";
export function NetworkImageWithTiers() {
return (
<NetworkImage
src="https://example.com/image-full.jpg"
mediumSrc="https://example.com/image-medium.jpg"
lowSrc="https://example.com/image-thumb.jpg"
alt="Adaptive image with quality tiers"
/>
);
}With badge
Show a live badge indicating the detected connection type.

4G
import { NetworkImage } from "@/components/network-image";
export function NetworkImageWithBadge() {
return (
<NetworkImage
src="https://picsum.photos/800/400"
alt="Image with network badge"
showBadge
/>
);
}