nexus-ui
uiimagesource: ok · hit
Image
Image renderer for URLs, base64, and byte payloads with preview, loader, lightbox, and action slots
npx shadcn add "https://nexus-ui.dev/r/image.json"free
Where it goes
testimonialgallery
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@radix-ui/react-slot@radix-ui/react-dialog
Registry dependencies
tooltipkbd
Files1 · source cached 10/15/2026, 7:59:57 AM
components/nexus-ui/image.tsx
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { Slot } from "@radix-ui/react-slot";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Kbd } from "@/components/ui/kbd";
import { cn } from "@/lib/utils";
type ImageData = {
src?: string;
base64?: string;
uint8Array?: Uint8Array;
mediaType?: string;
};
function resolveBase64MediaType(base64: string, fallback: string) {
const trimmed = base64.trim();
if (trimmed.startsWith("data:")) {
return /^data:([^;,]+)/i.exec(trimmed)?.[1] ?? fallback;
}
const normalized = trimmed.replace(/\s+/g, "").slice(0, 120);
if (normalized.startsWith("iVBORw0KGgo")) return "image/png";
if (normalized.startsWith("/9j/")) return "image/jpeg";
if (normalized.startsWith("R0lGOD")) return "image/gif";
if (normalized.startsWith("UklGR") && normalized.includes("V0VCUA")) {
return "image/webp";
}
if (normalized.startsWith("PHN2Zy") || normalized.startsWith("PD94bWwg")) {
return "image/svg+xml";
}
return fallback;
}
function toDataUrl(base64: string, mediaType: string) {
const trimmed = base64.trim();
if (trimmed.startsWith("data:")) return trimmed;
return `data:${mediaType};base64,${trimmed}`;
}
function uint8ArrayToBase64(bytes: Uint8Array) {
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.subarray(i, i + chunkSize);
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
type ImageContextValue = {
src?: string;
alt: string;
mediaType?: string;
hasError: boolean;
setHasError: (value: boolean) => void;
};
const ImageContext = React.createContext<ImageContextValue | null>(null);
function useImageContext(component: string) {
const ctx = React.useContext(ImageContext);
if (!ctx) {
throw new Error(`${component} must be used within <Image>`);
}
return ctx;
}
export type ImageProps = Omit<
React.HTMLAttributes<HTMLDivElement>,
"children"
> &
Omit<React.ComponentProps<typeof DialogPrimitive.Root>, "children"> &
ImageData & {
alt?: string;
children?: React.ReactNode;
};
function Image({
src,
base64,
uint8Array,
mediaType,
open,
defaultOpen,
onOpenChange,
modal,
alt = "",
className,
children,
...props
}: ImageProps) {
const hasUint8BlobSource =
!base64 && uint8Array != null && uint8Array.length > 0;
const { resolvedMediaType, resolvedSrc } = React.useMemo(() => {
const nextResolvedMediaType = base64
? resolveBase64MediaType(base64, mediaType ?? "image/png")
: (mediaType ?? "image/png");
const nextResolvedSrc = base64
? toDataUrl(base64, nextResolvedMediaType)
: hasUint8BlobSource
? toDataUrl(uint8ArrayToBase64(uint8Array), nextResolvedMediaType)
: src;
return {
resolvedMediaType: nextResolvedMediaType,
resolvedSrc: nextResolvedSrc,
};
}, [base64, mediaType, src, hasUint8BlobSource, uint8Array]);
const [hasError, setHasError] = React.useState(false);
const contextValue = React.useMemo<ImageContextValue>(
() => ({
src: resolvedSrc,
alt,
mediaType: resolvedMediaType,
hasError,
setHasError,
}),
[resolvedSrc, alt, resolvedMediaType, hasError],
);
return (
<ImageContext.Provider value={contextValue}>
<DialogPrimitive.Root
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
modal={modal}
>
<div
data-slot="image"
className={cn(
"group/image relative inline-flex aspect-auto max-w-full min-w-64 flex-col overflow-hidden rounded-2xl border dark:border-muted",
className,
)}
{...props}
>
{children ?? <ImagePreview />}
</div>
</DialogPrimitive.Root>
</ImageContext.Provider>
);
}
export type ImagePreviewProps = React.ImgHTMLAttributes<HTMLImageElement>;
function ImagePreview({
className,
src: srcProp,
alt: altProp,
onLoad,
onError,
...props
}: ImagePreviewProps) {
const { src: contextSrc, alt, setHasError } = useImageContext("ImagePreview");
const resolvedSrc = srcProp ?? contextSrc;
React.useEffect(() => {
setHasError(false);
}, [resolvedSrc, setHasError]);
if (!resolvedSrc) {
return <ImageLoader aria-hidden className={className} />;
}
return (
<DialogPrimitive.Trigger data-slot="image-preview-trigger" asChild>
<div data-slot="image-preview" className="relative size-full max-w-full">
<ImageLoader className="absolute inset-0 z-0" />
<img
{...props}
src={resolvedSrc}
alt={altProp ?? alt}
className={cn(
"relative z-1 size-full max-w-full overflow-hidden rounded-md object-cover",
className,
)}
onLoad={(e) => {
setHasError(false);
onLoad?.(e);
}}
onError={(e) => {
setHasError(true);
onError?.(e);
}}
/>
</div>
</DialogPrimitive.Trigger>
);
}
export type ImageLightboxProps = React.ComponentProps<
typeof DialogPrimitive.Portal
>;
function ImageLightbox(props: ImageLightboxProps) {
return <DialogPrimitive.Portal data-slot="image-lightbox" {...props} />;
}
export type ImageLightboxOverlayProps = React.ComponentProps<
typeof DialogPrimitive.Overlay
>;
function ImageLightboxOverlay({
className,
...props
}: ImageLightboxOverlayProps) {
return (
<DialogPrimitive.Overlay
data-slot="image-lightbox-overlay"
className={cn(
"fixed inset-0 z-50 bg-background/50 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
);
}
export type ImageLightboxPreviewProps = React.ComponentProps<
typeof DialogPrimitive.Content
> & {
src?: string;
alt?: string;
};
function ImageLightboxPreview({
className,
children,
onInteractOutside,
...props
}: ImageLightboxPreviewProps) {
const { src: contextSrc, alt } = useImageContext("ImageLightboxPreview");
const resolvedSrc = contextSrc;
return (
<DialogPrimitive.Content
data-slot="image-lightbox-preview"
className={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] overflow-hidden rounded-2xl duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg xl:max-w-2xl",
className,
)}
onInteractOutside={(e) => {
const target = e.target as HTMLElement | null;
if (target?.closest('[data-slot="image-actions"]')) {
e.preventDefault();
}
onInteractOutside?.(e);
}}
{...props}
>
<img
data-slot="image-lightbox-image"
src={resolvedSrc}
alt={alt}
className={cn("size-full object-contain")}
/>
{children}
<ImageLightboxTitle>{alt || "Image"}</ImageLightboxTitle>
</DialogPrimitive.Content>
);
}
export type ImageLightboxCloseProps = React.ComponentProps<
typeof DialogPrimitive.Close
>;
function ImageLightboxClose(props: ImageLightboxCloseProps) {
return (
<DialogPrimitive.Close
data-slot="image-lightbox-close"
className="sr-only"
{...props}
/>
);
}
function ImageLightboxTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="image-lightbox-title"
className={cn("sr-only", className)}
{...props}
/>
);
}
export type ImageLoaderProps = React.HTMLAttributes<HTMLDivElement>;
function ImageLoader({ className, ...props }: ImageLoaderProps) {
const { hasError } = useImageContext("ImageLoader");
return (
<div
data-slot="image-loader"
className={cn(
"size-full animate-pulse rounded-lg bg-input",
hasError && "bg-destructive/20",
className,
)}
{...props}
/>
);
}
export type ImageActionsAlign =
| "inline-start"
| "inline-end"
| "block-start"
| "block-end";
const imageActionsAlignStyles: Record<ImageActionsAlign, string> = {
"inline-start": "top-1/2 left-0 -translate-y-1/2 flex-col h-full",
"inline-end": "top-1/2 right-0 -translate-y-1/2 flex-col h-full",
"block-start": "top-0 left-1/2 -translate-x-1/2 w-full",
"block-end": "bottom-0 left-1/2 -translate-x-1/2 w-full",
};
export type ImageActionsProps = React.HTMLAttributes<HTMLDivElement> & {
align?: ImageActionsAlign;
};
function ImageActions({
align = "block-end",
className,
...props
}: ImageActionsProps) {
return (
<div
data-slot="image-actions"
className={cn(
"pointer-events-none absolute z-10 flex shrink-0 items-center justify-between px-3 py-3",
imageActionsAlignStyles[align],
className,
)}
{...props}
/>
);
}
type ImageActionGroupProps = React.HTMLAttributes<HTMLDivElement>;
function ImageActionGroup({ className, ...props }: ImageActionGroupProps) {
return (
<div
data-slot="image-action-group"
className={cn("flex items-center gap-1.5", className)}
{...props}
/>
);
}
export type ImageActionProps = React.HTMLAttributes<HTMLDivElement> & {
asChild?: boolean;
tooltip?:
| string
| {
content?: string;
side?: "top" | "right" | "bottom" | "left";
shortcut?: string;
};
};
function ImageAction({
asChild = false,
tooltip,
className,
...props
}: ImageActionProps) {
const Comp = asChild ? Slot : "div";
const { content, side, shortcut } =
typeof tooltip === "string" ? { content: tooltip } : tooltip ?? {};
if (!content) {
return (
<Comp
data-slot="image-action"
className={cn("pointer-events-auto", className)}
{...props}
/>
);
}
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
<Comp
data-slot="image-action"
className={cn("pointer-events-auto", className)}
{...props}
/>
</TooltipTrigger>
<TooltipContent className="rounded-full" side={side}>
{content}
{shortcut ? <Kbd className="rounded-md!">{shortcut}</Kbd> : null}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
Image.displayName = "Image";
ImagePreview.displayName = "ImagePreview";
ImageLightbox.displayName = "ImageLightbox";
ImageLightboxOverlay.displayName = "ImageLightboxOverlay";
ImageLightboxPreview.displayName = "ImageLightboxPreview";
ImageLightboxClose.displayName = "ImageLightboxClose";
ImageLoader.displayName = "ImageLoader";
ImageActions.displayName = "ImageActions";
ImageActionGroup.displayName = "ImageActionGroup";
ImageAction.displayName = "ImageAction";
export {
Image,
ImagePreview,
ImageLightbox,
ImageLightboxOverlay,
ImageLightboxPreview,
ImageLightboxClose,
ImageLoader,
ImageActions,
ImageActionGroup,
ImageAction,
};