Getting Started
App Providerglobal.d.tsThemesTypes
cn(...args)Text ParserUnits Converters
AnchorAvatarBreadcrumbBurgerButtonCardCarouselCheckerCodeColor PickerCommandConfettiCopyButtonDouble Helix WordsFloating IndicatorGroupHighlight TextIndicatorInputKbdLabelLoaderPaginationPassword RequirementPolymorphic SlotProgressProseRatingRunning AreaScroll AreaSheetsSkeletonSliderStackSvgTableTabsTextareaTimelineTimesToasterTooltipTyping WordsTypography
useClickOutsideuseClipboarduseDeviceInfouseDialoguseDidUpdateuseDirectionuseDisclosureuseDocumentTitleuseDocumentVisibilityuseElementInfouseEyeDropperuseFetchuseFullscreenuseGeoLocationuseHotkeysuseHoveruseIduseImagePopupuseInputStateuseIntersectionuseIntervaluseIsomorphicEffectuseListStateuseLocalStorageuseMeasureScrollbaruseMediaQueryuseMergedRefuseMouseuseMoveuseMutationObserveruseNetworkuseOpenStateuseOrientationuseOSusePaginationusePWAInstalleruseRandomColorsuseReducedMotionuseReloaduseResizeObserveruseScrollIntoViewuseStateHistoryuseTimeoutuseTouchuseTriggeruseUncontrolleduseValidatedStateuseViewportSizeuseWindowEventuseWindowScroll
Docs
Web
Hooks
useImagePopup
useId

Creates a unique ID for consistent DOM identification.

useInputState

Simplifies controlled input handling with value and onChange.


Edit this page on GitHub
  • Started
  • Utilities
  • Configuration
  • Components
  • Hooks
  • Examples
  • Github
  • Contributing
⌘+J

© 2025 oeri rights MIT


Designed in Earth-616

Built by oeri

useImagePopup

A hook that controls the logic for opening, closing, and navigating image popups or previews.

Usage

Basic usage example to quickly see how the useImagePopup works.

4N
BF
OL
Y
+999
"use client";
import { Stack } from "@/ui/stack";
import { Avatar } from "@/ui/avatar";
import { useImagePopup } from "@/hooks/use-image-popup";

export function UseImagePopupDemo() {
  useImagePopup("[data-has-popup]");

  return (
    <Stack align="center" gap={28}>
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img data-has-popup src="/icons/oeri-asset.png" alt="" width={96} height={96} className="size-24 cursor-zoom-in rounded-lg border bg-black" />

      <Avatar.Group size={70} gap={28}>
        <Avatar data-has-popup fallback="4ndrea" src="https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Andrea" />
        <Avatar data-has-popup fallback="Brian and Frend Paual" src="https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=Brian" />
        <Avatar data-has-popup fallback="oliver" src="https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=oliver" />
        <Avatar data-has-popup fallback="y" src="https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=y" />
        <Avatar data-has-popup initialLimit="4">+999</Avatar>
      </Avatar.Group>
    </Stack>
  );
}

Selectors

By default, selectors API using class="embeded-image"

function MyComponent() {
  useImagePopup();
 
  return (
    <article>
      <img className="embeded-image" src="https://.../a.png" alt="" width="50" height="50" />
      <img className="embeded-image" src="https://.../b.jpg" alt="" width="50" height="50" />
      <img className="embeded-image" src="https://.../c.svg" alt="" width="50" height="50" />
    </article>
  );
}

Use selectors by [attribute]

function MyComponent() {
  useImagePopup("[data-popup]");
 
  return (
    <article>
      <img data-popup="" src="https://.../a.png" alt="" width="50" height="50" />
      <img data-popup="" src="https://.../b.jpg" alt="" width="50" height="50" />
      <img data-popup="" src="https://.../c.svg" alt="" width="50" height="50" />
    </article>
  );
}

Use selectors by <img> Element

import Image from "next/image";
 
function MyComponent() {
  useImagePopup("img");
 
  return (
    <div>
      <article>
        <Image src="https://.../a.png" alt="" width="50" height="50" />
        <img src="https://.../b.jpg" alt="" width="50" height="50" />
        <img data-popup="" src="https://.../c.svg" alt="" width="50" height="50" />
      </article>
 
      <img className="embeded-image" src="https://.../x.png" alt="" width="50" height="50" />
      <Image className="" src="https://.../y.png" alt="" width="50" height="50" />
    </div>
  );
}

Depend On

  • useMeasureScrollbar

API

APITypeDefault
selectors?React.ElementType | (string & {})".embeded-image"
timeRender?numbertimeRender: 350

Source Codes

Full working code example, including necessary markup and styles. You can copy and paste this code directly to start using the component immediately.

use-image-popup.ts
globals.css
/* use-image-popup */ @layer base { .embeded-image { cursor: zoom-in; } .popup-container, .image-popup-overlay { display: flex; justify-content: center; align-items: center; inset: 0; width: 100%; height: 100%; overflow: hidden; transition: all var(--duration) ease 0ms; } .popup-container { position: fixed; z-index: 9999; } .image-popup-overlay { position: absolute; cursor: zoom-out; background-image: linear-gradient(to bottom, var(--overlay-bg-from, #0000), var(--overlay-bg-to, hsl(var(--pure-black) / 0.4))); } .image-popup { border-radius: var(--radius); height: auto; width: auto; max-width: 95dvw !important; max-height: 95dvh !important; z-index: 999 !important; background-color: var(--img-bg); position: relative; vertical-align: middle; object-fit: contain; transform-origin: center; --img-bg: #e5e5e5; } .image-popup[data-state="open"] { animation: zoom-state-open var(--duration, 0.3s) cubic-bezier(0.5, 0, 0.25, 1) normal forwards running; } .image-popup[data-state="closed"] { animation: zoom-state-closed var(--duration, 0.3s) cubic-bezier(0.5, 0, 0.25, 1) normal forwards running; } @keyframes zoom-state-open { from { -o-object-fit: unset; object-fit: unset; width: var(--initial-w); height: var(--initial-h); min-width: var(--initial-w); min-height: var(--initial-h); max-width: var(--initial-w); max-height: var(--initial-h); transform: translate(var(--translate-in)); background-color: #fff0; } to { width: auto; height: auto; transform: translate(0, 0); background-color: var(--img-bg); } } @keyframes zoom-state-closed { from { width: auto; height: auto; transform: translate(0, 0); background-color: var(--img-bg); } to { -o-object-fit: unset; object-fit: unset; width: var(--initial-w); height: var(--initial-h); min-width: var(--initial-w); min-height: var(--initial-h); max-width: var(--initial-w); max-height: var(--initial-h); transform: translate(var(--translate-in)); background-color: #fff0; } } }