GitHubX

Gallery

PreviousNext

A powerful carousel component that combines zoomable images with a fullscreen dialog and optional sidebar. Perfect for product galleries, image lightboxes, and media viewers with metadata.

      
      <script setup lang="ts">
import {
  Gallery,
  GalleryContent,
  GalleryImage,
  GalleryImageSource,
  GalleryItem,
  GalleryNext,
  GalleryPrevious,
  GalleryToolbar,
  GalleryViewer,
  GalleryViewerClose,
  GalleryViewerContent,
  GalleryViewerGallery,
  GalleryViewerSidebar,
  GalleryViewerSidebarTrigger,
  GalleryViewerTrigger,
  GalleryZoomInControl,
  GalleryZoomOutControl,
} from "@/components/gallery";
import { Image, ImageLoading, ImageSource } from "@/components/image";
import { SidebarContent, SidebarHeader, SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
import { ref } from "vue";

const scale = ref(1);
const initialIndex = ref(0);

const images = [
  "https://cdn.shopify.com/s/files/1/0745/1295/7658/files/IMG-18273962_m_jpeg_1.jpg?v=1760537131",
  "https://www.ghacks.net/wp-content/uploads/2025/10/M5-MacBook-Pro-keyboard.jpg",
  "https://istyle.mk/cdn/shop/files/IMG-18277044_m_jpeg_1.jpg?v=1760537022&width=823",
  "https://www.papita.co/om/wp-content/uploads/sites/22/2025/10/MACBOOKPROM5-3.jpg",
  "https://istyle.mk/cdn/shop/files/IMG-18277042_m_jpeg_1.jpg?v=1760537023&width=823",
  "https://cdn.pdx.stibosystems.com/submitted/assets/23a2f7d5-e63d-3d61-95ee-745b7c0c8dd0/IMG-18277141/IMG-18277141_m_jpeg_1.jpeg?v=1760536763133",
];
</script>

<template>
  <GalleryViewer default-sidebar-open>
    <div class="grid grid-cols-3 gap-2 w-full">
      <GalleryViewerTrigger v-for="(src, index) in images" :key="index" class="w-full bg-muted rounded-lg overflow-hidden cursor-pointer" @click="initialIndex = index">
        <Image class="size-full">
          <ImageSource :src="src" alt="Gallery image" class="w-full aspect-square object-cover hover:scale-110 transition-transform duration-300 animate-in fade-in" />
          <ImageLoading as-child>
            <div class="w-full aspect-square bg-muted animate-in fade-in" />
          </ImageLoading>
        </Image>
      </GalleryViewerTrigger>
    </div>

    <GalleryViewerContent :style="{ '--sidebar-width': '20rem', '--sidebar-width-mobile': '20rem' }">
      <GalleryViewerGallery>
        <Gallery
          v-model:zoom-scale="scale"
          :zoom-follow-cursor="true"
          :zoom-max-scale="2"
          :initial-index="initialIndex"
          zoom-on-click
          class="flex flex-col gap-6 h-full"
          v-slot="{ selectedIndex, isZoomed }"
        >
          <GalleryContent class="h-full items-center overflow-visible">
            <GalleryItem v-for="(src, index) in images" :key="index" :item-index="index + 1" class="flex justify-center overflow-visible pointer-events-auto">
              <GalleryImage class="basis-full md:basis-1/2 overflow-visible">
                <GalleryImageSource :src="src" alt="Gallery image" class="w-full h-full object-cover" />
              </GalleryImage>
            </GalleryItem>
          </GalleryContent>

          <GalleryToolbar class="absolute top-0 right-0 flex items-center gap-2">
            <GalleryZoomInControl class="text-white" />
            <GalleryZoomOutControl class="text-white" />
            <GalleryViewerSidebarTrigger class="text-white" />
            <GalleryViewerClose class="text-white" />
          </GalleryToolbar>

          <div class="absolute shadow bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 bg-muted text-sm text-muted-foreground rounded-full p-2 overflow-hidden">
            <div 
              v-for="(src, index) in images" 
              :key="index" 
              class="size-2 rounded-full" 
              :class="selectedIndex === index + 1 ? 'bg-primary' : 'bg-primary/30'" 
            />
          </div>

          <GalleryPrevious v-if="!isZoomed" class="hidden md:flex absolute left-4 top-1/2 -translate-y-1/2" />
          <GalleryNext v-if="!isZoomed" class="hidden md:flex absolute right-4 top-1/2 -translate-y-1/2" />
        </Gallery>
      </GalleryViewerGallery>

      <GalleryViewerSidebar side="right">
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem class="px-4 py-4 font-bold text-lg">
              Image Gallery
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent class="px-4">
          <p class="text-sm text-muted-foreground">
            Use the sidebar for metadata, image details, or custom actions. The sidebar can be toggled with the panel button in the toolbar.
          </p>
        </SidebarContent>
      </GalleryViewerSidebar>
    </GalleryViewerContent>
  </GalleryViewer>
</template>
    

Features

  • Carousel — Navigate between images with prev/next controls
  • Zoom — Zoom and pan on each image (pinch, click, cursor-following)
  • Fullscreen dialog — Open gallery in a fullscreen overlay
  • Sidebar — Optional sidebar for custom content (metadata, details, actions)
  • Navigation on zoom — Prev/next buttons hide when zoomed for cleaner UX

Installation

Install from the Vuzeno registry. Requires Image Viewer and its dependencies:

      
      bunx --bun shadcn-vue@latest add https://vuzeno.com/r/gallery.json
    

Examples

      
      <script setup lang="ts">
import { Gallery, GalleryContent, GalleryImage, GalleryImageSource, GalleryItem, GalleryNext, GalleryPrevious } from "@/components/gallery";
import { ref } from "vue";

const scale = ref(1);
const images = ["https://picsum.photos/id/229/600/400", "https://picsum.photos/id/230/600/400", "https://picsum.photos/id/231/600/400"];
</script>

<template>
  <Gallery v-model:zoom-scale="scale" zoom-on-click class="relative w-full max-w-2xl overflow-hidden flex flex-col gap-6" v-slot="{ selectedIndex }">
    <GalleryContent class="h-64 items-center overflow-visible">
      <GalleryItem v-for="(src, index) in images" :key="index" :item-index="index + 1" class="flex justify-center overflow-visible">
        <GalleryImage class="basis-1/2 overflow-visible">
          <GalleryImageSource :src="src" alt="Gallery image" class="w-full h-full object-cover" />
        </GalleryImage>
      </GalleryItem>
    </GalleryContent>

    <div class="mx-auto flex items-center gap-2">
      <GalleryPrevious class="relative translate-0 size-6" variant="secondary" />
      <div class="flex items-center gap-2 bg-muted rounded-full p-2 overflow-hidden">
        <div 
          v-for="(src, index) in images" 
          :key="index" 
          class="size-2 rounded-full" 
          :class="selectedIndex === index + 1 ? 'bg-primary' : 'bg-primary/30'" 
        />
      </div>
      <GalleryNext class="relative translate-0 size-6" variant="secondary" />
    </div>
  </Gallery>
</template>
    

Use Gallery, GalleryContent, GalleryItem, GalleryImage, and GalleryImageSource for a simple zoomable carousel:

      
      <template>
  <Gallery v-model:zoom-scale="scale" zoom-on-click>
    <GalleryContent class="h-64">
      <GalleryItem v-for="(src, index) in images" :key="index" :item-index="index + 1">
        <GalleryImage>
          <GalleryImageSource :src="src" alt="..." />
        </GalleryImage>
      </GalleryItem>
    </GalleryContent>
    <GalleryPrevious />
    <GalleryNext />
  </Gallery>
</template>
    

Within Dialog

      
      <script setup lang="ts">
import { Gallery, GalleryContent, GalleryImage, GalleryImageSource, GalleryItem, GalleryNext, GalleryPrevious, GalleryToolbar, GalleryZoomInControl, GalleryZoomOutControl } from "@/components/gallery";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Maximize2Icon, Minimize2Icon } from "lucide-vue-next";
import { ref } from "vue";

const scale = ref(1);
const initialIndex = ref(0);
const isExpanded = ref(false);

const images = [
  "https://cdn.shopify.com/s/files/1/0745/1295/7658/files/IMG-18273962_m_jpeg_1.jpg?v=1760537131",
  "https://www.ghacks.net/wp-content/uploads/2025/10/M5-MacBook-Pro-keyboard.jpg",
  "https://istyle.mk/cdn/shop/files/IMG-18277044_m_jpeg_1.jpg?v=1760537022&width=823",
  "https://www.papita.co/om/wp-content/uploads/sites/22/2025/10/MACBOOKPROM5-3.jpg",
  "https://istyle.mk/cdn/shop/files/IMG-18277042_m_jpeg_1.jpg?v=1760537023&width=823",
  "https://cdn.pdx.stibosystems.com/submitted/assets/23a2f7d5-e63d-3d61-95ee-745b7c0c8dd0/IMG-18277141/IMG-18277141_m_jpeg_1.jpeg?v=1760536763133",
];
</script>

<template>
  <Dialog>
    <DialogTrigger>
      <Button variant="outline">
        Open Gallery
      </Button>
    </DialogTrigger>
    
    <DialogContent class="w-full max-w-3xl overflow-hidden" :class="{ 'max-w-[90vw]': isExpanded }">
      <Gallery
        v-model:zoom-scale="scale"
        :zoom-follow-cursor="true"
        :zoom-max-scale="2"
        :initial-index="initialIndex"
        zoom-on-click
        class="flex flex-col gap-4 h-full"
        v-slot="{ selectedIndex, isZoomed }"
      >
        <GalleryContent class="h-full items-center">
          <GalleryItem v-for="(src, index) in images" :key="index" :item-index="index + 1" class="flex justify-center pointer-events-auto">
            <GalleryImage class="basis-full md:basis-1/2">
              <GalleryImageSource :src="src" alt="Gallery image" class="w-full h-full object-cover" />
            </GalleryImage>
          </GalleryItem>
        </GalleryContent>

        <GalleryToolbar class="flex items-center gap-2 mx-auto">
          <GalleryZoomInControl />
          <GalleryZoomOutControl />
          <Button variant="ghost" size="icon-sm" class="size-7" @click="isExpanded = !isExpanded">
            <Maximize2Icon v-if="!isExpanded" />
            <Minimize2Icon v-else />
          </Button>
        </GalleryToolbar>

        <GalleryPrevious v-if="!isZoomed" class="hidden md:flex absolute left-4 top-1/2 -translate-y-1/2" />
        <GalleryNext v-if="!isZoomed" class="hidden md:flex absolute right-4 top-1/2 -translate-y-1/2" />
      </Gallery>
    </DialogContent>
  </Dialog>
</template>
    

Fullscreen with Sidebar

Combine GalleryViewer, GalleryViewerTrigger, GalleryViewerContent, and GalleryViewerSidebar for a fullscreen experience with custom sidebar content:

      
      <template>
  <GalleryViewer v-model:open="open">
    <GalleryViewerTrigger>
      <Button>Open Gallery</Button>
    </GalleryViewerTrigger>

    <GalleryViewerContent :style="{ '--sidebar-width': '24rem' }">
      <GalleryViewerGallery>
        <Gallery v-model:zoom-scale="scale" zoom-on-click>
          <GalleryContent>...</GalleryContent>
          <GalleryToolbar>
            <GalleryZoomInControl />
            <GalleryZoomOutControl />
            <GalleryViewerSidebarTrigger />
            <GalleryViewerClose />
          </GalleryToolbar>
          <GalleryPrevious />
          <GalleryNext />
        </Gallery>
      </GalleryViewerGallery>

      <GalleryViewerSidebar side="right">
        <SidebarHeader>...</SidebarHeader>
        <SidebarContent>Custom metadata, details, etc.</SidebarContent>
      </GalleryViewerSidebar>
    </GalleryViewerContent>
  </GalleryViewer>
</template>