Skip to main content
The Qwik adapter provides hooks for implementing zoom image functionality in Qwik applications with resumable, serializable state.

Installation

npm install @zoom-image/qwik

Available Hooks

The Qwik adapter exports four hooks that correspond to different zoom behaviors:
  • useZoomImageWheel - Zoom with mouse wheel
  • useZoomImageHover - Zoom on hover with separate zoom target
  • useZoomImageMove - Zoom on mouse move
  • useZoomImageClick - Zoom on click

useZoomImageWheel

Enables zooming with mouse wheel/trackpad scrolling.

API

function useZoomImageWheel(): {
  createZoomImage: QRL<(...args: Parameters<typeof createZoomImageWheel>) => void>
  zoomImageState: ZoomImageWheelState
  setZoomImageState: QRL<(state: ZoomImageWheelStateUpdate) => void>
}

State

interface ZoomImageWheelState {
  currentZoom: number
  enable: boolean
  currentPositionX: number
  currentPositionY: number
  currentRotation: number
}

Example

import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"
import { useZoomImageWheel } from "@zoom-image/qwik"

export default component$(() => {
  const imageContainerRef = useSignal<HTMLDivElement>()
  
  const {
    createZoomImage,
    zoomImageState,
    setZoomImageState
  } = useZoomImageWheel()
  
  useVisibleTask$(({ track }) => {
    track(() => imageContainerRef.value)
    
    if (imageContainerRef.value) {
      createZoomImage(imageContainerRef.value)
    }
  })
  
  return (
    <div>
      <p>Current zoom: {Math.round(zoomImageState.currentZoom * 100)}%</p>
      <p>Scroll inside the image to zoom</p>
      
      <div ref={imageContainerRef} class="h-[300px] w-[200px]">
        <img src="/image.jpg" alt="Zoomable image" class="h-full w-full" />
      </div>
      
      <div class="flex gap-2">
        <button
          onClick$={() => {
            setZoomImageState({
              currentZoom: zoomImageState.currentZoom + 0.5
            })
          }}
        >
          Zoom In
        </button>
        <button
          onClick$={() => {
            setZoomImageState({
              currentZoom: zoomImageState.currentZoom - 0.5
            })
          }}
        >
          Zoom Out
        </button>
        <button
          onClick$={() => {
            setZoomImageState({
              currentRotation: zoomImageState.currentRotation + 90
            })
          }}
        >
          Rotate
        </button>
      </div>
    </div>
  )
})

useZoomImageHover

Displays a zoomed version in a separate container when hovering over the image.

API

function useZoomImageHover(): {
  createZoomImage: QRL<(...args: Parameters<typeof createZoomImageHover>) => void>
  zoomImageState: ZoomImageHoverState
  setZoomImageState: QRL<(state: ZoomImageHoverStateUpdate) => void>
}

State

interface ZoomImageHoverState {
  enabled: boolean
  zoomedImgStatus: "idle" | "loading" | "loaded" | "error"
}

Example

import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"
import { useZoomImageHover } from "@zoom-image/qwik"

export default component$(() => {
  const imageContainerRef = useSignal<HTMLDivElement>()
  const zoomTargetRef = useSignal<HTMLDivElement>()
  
  const { createZoomImage, zoomImageState } = useZoomImageHover()
  
  useVisibleTask$(({ track }) => {
    track(() => imageContainerRef.value)
    track(() => zoomTargetRef.value)
    
    if (imageContainerRef.value && zoomTargetRef.value) {
      createZoomImage(imageContainerRef.value, {
        zoomImageSource: "/image-large.jpg",
        customZoom: { width: 400, height: 600 },
        zoomTarget: zoomTargetRef.value,
        scale: 2
      })
    }
  })
  
  return (
    <div class="flex gap-4">
      <div ref={imageContainerRef} class="relative h-[300px] w-[200px]">
        <img src="/image-small.jpg" alt="Hover to zoom" class="h-full w-full" />
      </div>
      
      <div ref={zoomTargetRef} class="absolute left-[250px]"></div>
    </div>
  )
})

useZoomImageMove

Zooms the image as the mouse moves over it.

API

function useZoomImageMove(): {
  createZoomImage: QRL<(...args: Parameters<typeof createZoomImageMove>) => void>
  zoomImageState: ZoomImageMoveState
}

State

interface ZoomImageMoveState {
  zoomedImgStatus: "idle" | "loading" | "loaded" | "error"
}

Example

import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"
import { useZoomImageMove } from "@zoom-image/qwik"

export default component$(() => {
  const imageContainerRef = useSignal<HTMLDivElement>()
  
  const { createZoomImage, zoomImageState } = useZoomImageMove()
  
  useVisibleTask$(({ track }) => {
    track(() => imageContainerRef.value)
    
    if (imageContainerRef.value) {
      createZoomImage(imageContainerRef.value, {
        zoomImageSource: "/image-large.jpg"
      })
    }
  })
  
  return (
    <div ref={imageContainerRef} class="relative h-[300px] w-[200px] overflow-hidden">
      <img src="/image.jpg" alt="Move mouse to zoom" class="h-full w-full" />
    </div>
  )
})

useZoomImageClick

Toggles zoom when clicking on the image.

API

function useZoomImageClick(): {
  createZoomImage: QRL<(...args: Parameters<typeof createZoomImageClick>) => void>
  zoomImageState: ZoomImageClickState
}

State

interface ZoomImageClickState {
  zoomedImgStatus: "idle" | "loading" | "loaded" | "error"
}

Example

import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik"
import { useZoomImageClick } from "@zoom-image/qwik"

export default component$(() => {
  const imageContainerRef = useSignal<HTMLDivElement>()
  
  const { createZoomImage, zoomImageState } = useZoomImageClick()
  
  useVisibleTask$(({ track }) => {
    track(() => imageContainerRef.value)
    
    if (imageContainerRef.value) {
      createZoomImage(imageContainerRef.value, {
        zoomImageSource: "/image-large.jpg"
      })
    }
  })
  
  return (
    <div ref={imageContainerRef} class="relative h-[300px] w-[200px] overflow-hidden cursor-pointer">
      <img src="/image.jpg" alt="Click to zoom" class="h-full w-full" />
    </div>
  )
})

Cleanup

All hooks automatically handle cleanup when the component unmounts using Qwik’s cleanup function in useVisibleTask$. The cleanup removes event listeners and frees resources.
useVisibleTask$(({ cleanup }) => {
  cleanup(() => {
    // Resources are freed automatically
  })
})

Qwik-Specific Patterns

Using $ for Event Handlers

Qwik requires the $ suffix for event handlers to enable lazy loading:
<button onClick$={() => {
  setZoomImageState({ currentZoom: 2 })
}}>Zoom</button>

Tracking Reactive Dependencies

Use track() in useVisibleTask$ to re-run effects when dependencies change:
useVisibleTask$(({ track }) => {
  track(() => zoomType.value)
  
  // Re-runs when zoomType changes
  if (zoomType.value === "wheel") {
    createZoomImageWheel(containerRef.value)
  }
})

Non-Serializable Values

The zoom instance is non-serializable and uses noSerialize() internally. The state itself is serializable and resumable.

Combining Multiple Zoom Types

import { component$, useSignal, useComputed$, useVisibleTask$ } from "@builder.io/qwik"
import {
  useZoomImageWheel,
  useZoomImageHover,
  useZoomImageMove,
  useZoomImageClick
} from "@zoom-image/qwik"

type Tab = {
  name: string
  current: boolean
  value: "wheel" | "hover" | "move" | "click"
}

export default component$(() => {
  const tabs = useSignal<Tab[]>([
    { name: "Wheel", current: true, value: "wheel" },
    { name: "Hover", current: false, value: "hover" },
    { name: "Move", current: false, value: "move" },
    { name: "Click", current: false, value: "click" }
  ])
  
  const imageWheelContainerRef = useSignal<HTMLDivElement>()
  const imageHoverContainerRef = useSignal<HTMLDivElement>()
  const imageMoveContainerRef = useSignal<HTMLDivElement>()
  const imageClickContainerRef = useSignal<HTMLDivElement>()
  const zoomTargetRef = useSignal<HTMLDivElement>()
  
  const { createZoomImage: createZoomImageWheel } = useZoomImageWheel()
  const { createZoomImage: createZoomImageHover } = useZoomImageHover()
  const { createZoomImage: createZoomImageMove } = useZoomImageMove()
  const { createZoomImage: createZoomImageClick } = useZoomImageClick()
  
  const zoomType = useComputed$(() => {
    return tabs.value.find(tab => tab.current)?.value || "wheel"
  })
  
  useVisibleTask$(({ track }) => {
    track(() => zoomType.value)
    
    if (zoomType.value === "wheel" && imageWheelContainerRef.value) {
      createZoomImageWheel(imageWheelContainerRef.value)
    }
    
    if (zoomType.value === "hover" && imageHoverContainerRef.value && zoomTargetRef.value) {
      createZoomImageHover(imageHoverContainerRef.value, {
        zoomImageSource: "/image-large.jpg",
        customZoom: { width: 300, height: 500 },
        zoomTarget: zoomTargetRef.value,
        scale: 2
      })
    }
    
    if (zoomType.value === "move" && imageMoveContainerRef.value) {
      createZoomImageMove(imageMoveContainerRef.value, {
        zoomImageSource: "/image-large.jpg"
      })
    }
    
    if (zoomType.value === "click" && imageClickContainerRef.value) {
      createZoomImageClick(imageClickContainerRef.value, {
        zoomImageSource: "/image-large.jpg"
      })
    }
  })
  
  return (
    <div>
      <nav class="flex gap-4 mb-4">
        {tabs.value.map((tab) => (
          <button
            key={tab.name}
            onClick$={() => {
              tabs.value = tabs.value.map(t => ({
                ...t,
                current: t.name === tab.name
              }))
            }}
            class={tab.current ? "active" : ""}
          >
            {tab.name}
          </button>
        ))}
      </nav>
      
      {zoomType.value === "wheel" && (
        <div ref={imageWheelContainerRef} class="h-[300px] w-[200px]">
          <img src="/image.jpg" alt="Wheel zoom" class="h-full w-full" />
        </div>
      )}
      
      {zoomType.value === "hover" && (
        <div class="flex gap-4">
          <div ref={imageHoverContainerRef} class="h-[300px] w-[200px]">
            <img src="/image.jpg" alt="Hover zoom" class="h-full w-full" />
          </div>
          <div ref={zoomTargetRef}></div>
        </div>
      )}
      
      {zoomType.value === "move" && (
        <div ref={imageMoveContainerRef} class="h-[300px] w-[200px] overflow-hidden">
          <img src="/image.jpg" alt="Move zoom" class="h-full w-full" />
        </div>
      )}
      
      {zoomType.value === "click" && (
        <div ref={imageClickContainerRef} class="h-[300px] w-[200px] overflow-hidden">
          <img src="/image.jpg" alt="Click zoom" class="h-full w-full" />
        </div>
      )}
    </div>
  )
})

TypeScript Support

The Qwik adapter is written in TypeScript and provides full type definitions. Import types from @zoom-image/core:
import type {
  ZoomImageWheelState,
  ZoomImageWheelStateUpdate,
  ZoomImageHoverState,
  ZoomImageHoverStateUpdate,
  ZoomImageMoveState,
  ZoomImageClickState
} from "@zoom-image/core"

Build docs developers (and LLMs) love