LLM index: /llms.txt

Image

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/image
pnpm dlx shadcn@latest add @nexus-ui/image
yarn dlx shadcn@latest add @nexus-ui/image
bunx shadcn@latest add @nexus-ui/image

Install the following dependencies:

npx shadcn@latest add tooltip kbd && npm install @radix-ui/react-slot @radix-ui/react-dialog
pnpm dlx shadcn@latest add tooltip kbd && pnpm add @radix-ui/react-slot @radix-ui/react-dialog
yarn dlx shadcn@latest add tooltip kbd && yarn add @radix-ui/react-slot @radix-ui/react-dialog
bunx shadcn@latest add tooltip kbd && bun add @radix-ui/react-slot @radix-ui/react-dialog

Copy and paste the following code into your project.

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,
};

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" />
  );
}

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/openai

Create your chat route

app/api/chat/route.ts
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

View as markdown Edit on GitHub