LLM index: /llms.txt
Composable image primitives for multimodal UIs. Image accepts AI-generated payloads (base64 or uint8Array) and exposes ImagePreview, ImageLoader, and optional action slots.
import { Image } from "@/components/nexus-ui/image";
export default function ImageDefault() {
const base64Image =
"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC";
return (
<Image
base64={base64Image}
mediaType="image/png"
uint8Array={new Uint8Array([])}
alt="Picture of a ball"
className="aspect-square w-1/2"
/>
);
}
Installation
npx shadcn@latest add @nexus-ui/imagepnpm dlx shadcn@latest add @nexus-ui/imageyarn dlx shadcn@latest add @nexus-ui/imagebunx shadcn@latest add @nexus-ui/imageInstall the following dependencies:
npx shadcn@latest add tooltip kbd && npm install @radix-ui/react-slot @radix-ui/react-dialogpnpm dlx shadcn@latest add tooltip kbd && pnpm add @radix-ui/react-slot @radix-ui/react-dialogyarn dlx shadcn@latest add tooltip kbd && yarn add @radix-ui/react-slot @radix-ui/react-dialogbunx shadcn@latest add tooltip kbd && bun add @radix-ui/react-slot @radix-ui/react-dialogCopy and paste the following code into your project.
"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,
};
Update import paths to match your project setup.
Usage
import {
Image,
ImagePreview,
ImageLightbox,
ImageLightboxOverlay,
ImageLightboxPreview,
ImageLightboxClose,
ImageLoader,
ImageActions,
ImageActionGroup,
ImageAction,
} from "@/components/nexus-ui/image";<Image
base64={base64Image}
mediaType="image/png"
uint8Array={new Uint8Array([])}
alt="Generated image"
/>Examples
With Overlay Actions
Compose ImagePreview with ImageActions, ImageActionGroup, and ImageAction for top/bottom/side controls. ImageAction supports built-in tooltips via tooltip as a string or object (content, optional side, optional shortcut).
import {
Image,
ImageAction,
ImageActionGroup,
ImageActions,
ImagePreview,
} from "@/components/nexus-ui/image";
import { Button } from "@/components/ui/button";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Copy01Icon,
Share01Icon,
Download01Icon,
} from "@hugeicons/core-free-icons";
export default function ImageWithActions() {
const base64Image =
"iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAADMElEQVR4nOzVwQnAIBQFQYXff81RUkQCOyDj1YOPnbXWPmeTRef+/3O/OyBjzh3CD95BfqICMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMK0CMO0TAAD//2Anhf4QtqobAAAAAElFTkSuQmCC";
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-4 sm:flex-row">
<Image
base64={base64Image}
mediaType="image/png"
uint8Array={new Uint8Array([])}
alt="Picture of a ball"
className="aspect-square w-1/2 min-w-0"
>
<ImagePreview />
<ImageActions align="block-start" className="justify-start">
<ImageActionGroup>
<ImageAction asChild tooltip="Share image">
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Share01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
<ImageAction asChild tooltip={{ content: "Copy image", shortcut: "C" }}>
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Copy01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
</ImageActionGroup>
</ImageActions>
<ImageActions align="block-end">
<ImageActionGroup>
<ImageAction asChild tooltip={{ content: "Edit", side: "top" }}>
<Button
type="button"
variant="secondary"
className="h-8 cursor-pointer rounded-full bg-card/90 px-3 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
Edit
</Button>
</ImageAction>
</ImageActionGroup>
<ImageActionGroup>
<ImageAction
asChild
tooltip={{ content: "Download image", side: "left", shortcut: "D" }}
>
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Download01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
</ImageActionGroup>
</ImageActions>
</Image>
<Image
base64={base64Image}
mediaType="image/png"
uint8Array={new Uint8Array([])}
alt="Picture of a ball"
className="aspect-square w-1/2 min-w-0"
>
<ImagePreview />
<ImageActions align="inline-start" className="justify-center">
<ImageActionGroup className="flex-col">
<ImageAction asChild tooltip={{ content: "Share image", side: "right" }}>
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Share01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
<ImageAction asChild tooltip="Copy image">
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Copy01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
</ImageActionGroup>
</ImageActions>
<ImageActions align="inline-end" className="items-end">
<ImageActionGroup>
<ImageAction asChild tooltip="Edit">
<Button
type="button"
variant="secondary"
className="h-8 cursor-pointer rounded-full bg-card/90 px-3 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
Edit
</Button>
</ImageAction>
</ImageActionGroup>
<ImageActionGroup>
<ImageAction asChild tooltip={{ content: "Download image", shortcut: "D" }}>
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-card/90 text-[13px] text-primary backdrop-blur-lg hover:bg-card active:scale-97"
>
<HugeiconsIcon
icon={Download01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageAction>
</ImageActionGroup>
</ImageActions>
</Image>
</div>
);
}
External URL
Use src for regular image URLs when you want Image to behave like a standard <img> source renderer.
import { Image } from "@/components/nexus-ui/image";
export default function ImageExternalSrc() {
return (
<Image
src="https://images.unsplash.com/photo-1663162221489-385e5d75d29f?q=80&w=1180&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
alt="Image loaded from external URL"
className="aspect-square w-1/2"
/>
);
}
No Source Placeholder
Render Image without base64 or uint8Array source to show the built-in placeholder loader.
import { Image } from "@/components/nexus-ui/image";
export default function ImageNoSourcePlaceholder() {
return (
<Image alt="No image source provided" className="aspect-square w-1/2" />
);
}
Lightbox Preview
ImagePreview acts as the trigger, while ImageLightbox renders portal primitives (ImageLightboxOverlay, ImageLightboxPreview) and ImageLightboxClose can be placed externally in overlay actions.
import {
Image,
ImageLightbox,
ImageLightboxOverlay,
ImageLightboxClose,
ImageLightboxPreview,
ImagePreview,
ImageActions,
ImageActionGroup,
ImageAction,
} from "@/components/nexus-ui/image";
import { base64Image } from "@/components/nexus-ui/examples/image/base64";
import { Button } from "@/components/ui/button";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
export default function ImageLightboxExample() {
return (
<Image
base64={base64Image}
mediaType="image/png"
alt="Preview that opens in a lightbox"
className="aspect-square w-1/2"
>
<ImagePreview className="cursor-pointer" />
<ImageLightbox>
<ImageLightboxOverlay />
<ImageLightboxPreview />
<ImageActions align="block-start" className="fixed z-60 justify-end">
<ImageActionGroup>
<ImageAction asChild>
<ImageLightboxClose asChild>
<Button
type="button"
size="icon-sm"
variant="secondary"
className="cursor-pointer rounded-full bg-secondary text-[13px] text-primary backdrop-blur-lg hover:bg-secondary/80 active:scale-97"
>
<HugeiconsIcon
icon={Cancel01Icon}
strokeWidth={2.0}
className="size-4"
/>
</Button>
</ImageLightboxClose>
</ImageAction>
</ImageActionGroup>
</ImageActions>
</ImageLightbox>
</Image>
);
}
Vercel AI SDK Integration
Use Image with OpenAI image generation by returning base64 and mediaType from your API route, then passing them directly to Image.
Install the AI SDK
npm install ai @ai-sdk/openaiCreate your chat route
import { openai } from "@ai-sdk/openai";
import { generateImage } from "ai";
export async function POST(req: Request) {
const { prompt }: { prompt: string } = await req.json();
const { image } = await generateImage({
model: openai.image("dall-e-3"),
prompt: prompt,
size: "1024x1024",
});
return Response.json({
base64: image.base64,
uint8Array: image.uint8Array ? Array.from(image.uint8Array) : undefined,
mediaType: image.mediaType,
});
}Call the route and render the generated image
"use client";
import { useState } from "react";
import { Image } from "@/components/nexus-ui/image";
type GeneratedImage = {
base64?: string;
uint8Array?: number[];
mediaType?: string;
};
export function ChatWithImages() {
const [prompt, setPrompt] = useState("A futuristic city skyline at sunset");
const [generatedImage, setGeneratedImage] = useState<GeneratedImage | null>(null);
const binaryImage = generatedImage?.uint8Array
? new Uint8Array(generatedImage.uint8Array)
: undefined;
async function onGenerate() {
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
const data = (await response.json()) as GeneratedImage;
setGeneratedImage(data);
}
return (
<div className="space-y-3">
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
className="w-full rounded-md border px-3 py-2"
/>
<button className="rounded-md border px-3 py-2" onClick={onGenerate}>
Generate
</button>
{generatedImage?.base64 || binaryImage ? (
<Image
base64={generatedImage.base64}
uint8Array={binaryImage}
mediaType={generatedImage.mediaType ?? "image/png"}
alt="Generated image"
className="h-64 w-full"
/>
) : null}
</div>
);
}API Reference
Image
Root container that resolves image source from base64 or uint8Array, manages shared context, and wraps Dialog Root for lightbox state.
Prop
Type
ImagePreview
Renders the <img> element using source/alt from Image context and wraps Dialog Trigger. If there is no resolved source, it renders the loader placeholder.
Prop
Type
Also extends standard img props.
ImageLightbox
Portal root for lightbox content. Wraps Dialog Portal.
Prop
Type
ImageLightboxOverlay
Backdrop layer for the lightbox. Wraps Dialog Overlay.
Prop
Type
ImageLightboxPreview
Centered lightbox content surface that renders the image and optional children. Wraps Dialog Content.
Prop
Type
ImageLightboxClose
Close control primitive for the lightbox. Wraps Dialog Close.
Prop
Type
ImageLoader
Pulsing loader surface used by ImagePreview (both as background while image paints and as placeholder when no source exists).
Prop
Type
ImageActions
Absolute-positioned actions container for overlay controls.
Prop
Type
ImageActionGroup
Horizontal grouping wrapper for related overlay actions.
Prop
Type
ImageAction
Wrapper for one action item. Supports polymorphism via asChild and optional built-in tooltip rendering.
Prop
Type