# Nuxt Photo > Photo galleries, lightboxes, and carousels for Nuxt with predictable SSR layouts and real image data. ## When to use Use this site to build photo galleries, lightboxes, and carousels with Nuxt Photo. ## Documentation - [Introduction](https://nuxt-photo.lupinum.com/raw/docs/start/introduction.md): Photo albums, a lightbox, and a carousel for Nuxt. Server-rendered layout from known image sizes, one photo shape for every component, and which component to use. - [Get started](https://nuxt-photo.lupinum.com/raw/docs/start/get-started.md): Install the Nuxt module, paste a complete album page, and check the lightbox. Then replace the sample photos with your own. - [Examples](https://nuxt-photo.lupinum.com/raw/docs/start/examples.md): Complete, copyable components that run on the page - portfolio grid with captions and crossfade, photos in an article, product carousel, Tailwind lightbox, custom caption and counter, and a custom viewer. - [AI coding agents](https://nuxt-photo.lupinum.com/raw/docs/start/ai-agents.md): Give Claude Code, Codex, Cursor, and other agents the Nuxt Photo docs for the installed version through one AGENTS.md pointer, plus llms.txt, Markdown pages, and the docs MCP server. - [Use Nuxt Photo with Vue](https://nuxt-photo.lupinum.com/raw/docs/start/plain-vue.md): Use the components in a plain Vue 3 app with Vite - install @lupinum/vue-photo, import components and the stylesheet, set labels, lightbox and image provider once with createPhoto, and deliver images without Nuxt Image. - [Compare gallery approaches](https://nuxt-photo.lupinum.com/raw/docs/start/compare-approaches.md): Nuxt Photo compared with PhotoSwipe, lightGallery, Nuxt Image, and a custom stack - album layout, lightbox, carousel, image delivery, video, and licensing. - [Performance and bundle size](https://nuxt-photo.lupinum.com/raw/docs/start/performance-and-bundle-size.md): Measured client bundle size for the Nuxt module, PhotoAlbum, and single Vue imports, and how to keep image cost low. - [Use your own photos](https://nuxt-photo.lupinum.com/raw/docs/guides/use-your-photos.md): Show a folder from public/ with usePhotoFolder, map CMS or API records to PhotoItem, get real image width and height at upload or from localImages, keep IDs stable, type custom meta data, and handle invalid records with validation="drop". - [Responsive layouts and SSR](https://nuxt-photo.lupinum.com/raw/docs/guides/responsive-layouts.md): Choose rows, columns, masonry, grid, bento, mosaic, or accordion, set row height, columns, and spacing per container width with responsive(), crop photos into tiles with a featured photo or a +N count, and prevent layout shift during SSR and hydration with defaultContainerWidth and breakpoints. - [Share one lightbox](https://nuxt-photo.lupinum.com/raw/docs/guides/share-one-lightbox.md): Let several albums and single photos on one page share one lightbox and one next/previous order with PhotoGroup. Collection rules, missing IDs, and turning the lightbox off. - [Build a carousel](https://nuxt-photo.lupinum.com/raw/docs/guides/build-a-carousel.md): Horizontal photo browsing with PhotoCarousel - thumbnails, arrows, dots, loop, autoplay, several visible slides, right-to-left pages, custom controls, and an optional lightbox. - [Deliver images](https://nuxt-photo.lupinum.com/raw/docs/guides/deliver-images.md): How Nuxt Photo sizes every thumbnail, slide and photo - srcset from your Nuxt Image screens, sizes from the layout, priority and preload for the first screen, build-time blur placeholders, browser cache headers, and the Nuxt Image config for Vercel, static sites, IPX and Cloudinary. - [Integrate a custom image service](https://nuxt-photo.lupinum.com/raw/docs/guides/integrate-a-custom-image-service.md): Connect your own CDN, CMS image API, signed URLs or hand-built Cloudinary URLs with a PhotoProvider - a url function per width, optional placeholders, fixed renditions with srcset, a Nuxt Image custom provider for the whole app, per-component providers, and unit tests. - [Style and theme](https://nuxt-photo.lupinum.com/raw/docs/guides/style-and-theme.md): Change the look of albums and the lightbox - css all vs structure, CSS variables for colors, buttons, mat, glow and placeholders, ui classes, scoped styles, dark themes, and Tailwind CSS with a complete Tailwind lightbox. - [Customize the lightbox](https://nuxt-photo.lupinum.com/raw/docs/guides/customize-the-built-in-lightbox.md): Change the built-in lightbox without rebuilding it - download, share and full screen buttons, custom caption, counter, tool and action buttons with slots, slide overlays, fade or crossfade navigation, one lightbox for the whole app, and when to use lower-level components instead. - [Create a custom thumbnail layout](https://nuxt-photo.lupinum.com/raw/docs/guides/create-a-custom-thumbnail-layout.md): Render thumbnails in any layout you build yourself - a grid, a hexagon grid, a list - and keep the built-in lightbox, keyboard access, and opening animation with LightboxProvider, PhotoTrigger, and PhotoImage. - [Build a lightbox from primitives](https://nuxt-photo.lupinum.com/raw/docs/guides/build-a-lightbox-from-primitives.md): Write your own lightbox markup and CSS with LightboxRoot, LightboxViewport, LightboxSlide, LightboxControls, and LightboxCaption, while Nuxt Photo keeps state, gestures, zoom, focus, and the opening animation. Frame variables and an accessibility checklist. - [Control the lightbox from code](https://nuxt-photo.lupinum.com/raw/docs/guides/control-the-lightbox.md): Open the lightbox at a given photo from a button, bind the active photo with v-model:active, close it with the browser Back button, link to a photo with deepLink, and move next and previous with template refs or useLightbox(). - [Reference](https://nuxt-photo.lupinum.com/raw/docs/reference.md): Where to look up exact names, props, defaults, events, slots, composables, types, module options, CSS variables, and package exports. - [Photo](https://nuxt-photo.lupinum.com/raw/docs/reference/photo.md): The Photo component - props, slots, and behavior for one photo with an optional one-photo lightbox, alone or inside a PhotoGroup. - [PhotoAlbum](https://nuxt-photo.lupinum.com/raw/docs/reference/photo-album.md): The PhotoAlbum component - props, layout options, events, the thumbnail slot, and the template ref for rows, columns, masonry, grid, bento, mosaic, and accordion albums. - [PhotoGroup](https://nuxt-photo.lupinum.com/raw/docs/reference/photo-group.md): The PhotoGroup component - props, events, slot, and template ref for one lightbox and navigation order across several albums and photos. - [PhotoCarousel](https://nuxt-photo.lupinum.com/raw/docs/reference/photo-carousel.md): The PhotoCarousel component - every prop, event, and slot for thumbnails, arrows, dots, loop, autoplay, controls, and the optional lightbox. - [Lightbox](https://nuxt-photo.lupinum.com/raw/docs/reference/lightbox.md): The built-in Lightbox component - slots, keyboard shortcuts, mouse and touch gestures, zoom, opening animation, slide/fade/crossfade navigation, reduced motion, and focus behavior. - [Primitives](https://nuxt-photo.lupinum.com/raw/docs/reference/primitives.md): Lower-level lightbox components for your own markup - LightboxProvider, LightboxRoot, LightboxOverlay, LightboxAmbient, LightboxViewport, LightboxSlide, LightboxControls, LightboxCaption, PhotoTrigger, PhotoImage. - [Composables](https://nuxt-photo.lupinum.com/raw/docs/reference/composables.md): Public helpers for lightbox state, reactive labels, async local folders, photo validation, providers, and responsive values. - [Configuration and labels](https://nuxt-photo.lupinum.com/raw/docs/reference/configuration-and-labels.md): PhotoConfig, Nuxt module options, providers, lightbox defaults, precedence, ten locales, and label templates. - [Types](https://nuxt-photo.lupinum.com/raw/docs/reference/types.md): The complete public types for photos, providers, configuration, gallery handles, controllers, layouts, validation, labels, UI classes, and slots. - [CSS](https://nuxt-photo.lupinum.com/raw/docs/reference/css.md): Every CSS class and CSS variable - lightbox mat, colors, buttons, glow, carousel, album - and the none, structure, and all CSS modes. - [Package exports](https://nuxt-photo.lupinum.com/raw/docs/reference/package-exports.md): The supported import paths for Nuxt (@lupinum/nuxt-photo/app) and plain Vue (@lupinum/vue-photo), and everything each one exports. - [Troubleshooting](https://nuxt-photo.lupinum.com/raw/docs/help/troubleshooting.md): Fix Nuxt Photo problems by error message or by what you see - invalid photo data, missing IDs in a PhotoGroup, missing lightbox context, Nuxt Image errors, hydration mismatches, unstyled albums, layout jumps, and lightbox clicks. - [Known constraints](https://nuxt-photo.lupinum.com/raw/docs/help/known-constraints.md): What Nuxt Photo 1.0 does not do and which options are read only at mount - known image sizes, photos only, setup-time lightbox, one open lightbox, required structure CSS, limited carousel options, Vercel image limits, and sizes=auto support. --- Source: https://nuxt-photo.lupinum.com/docs/start/introduction --- title: "Introduction" description: "Photo albums, a lightbox, and a carousel for Nuxt. Server-rendered layout from known image sizes, one photo shape for every component, and which component to use." url: "https://nuxt-photo.lupinum.com/docs/start/introduction" route: "/docs/start/introduction" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Introduction > Photo albums, a lightbox, and a carousel for Nuxt. Server-rendered layout from known image sizes, one photo shape for every component, and which component to use. Nuxt Photo renders photo albums and a carousel, and opens them in an accessible lightbox. It lays out each album on the server from the image sizes you already know, so the page does not jump while images load. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/PortfolioGrid.vue] ``` Select a photo. Try the arrow keys, a click to zoom, and `Escape`: focus returns to the photo you were viewing. ## Pick a component All components take the same photo data and are registered for you in Nuxt. | You need | Use | Lightbox | | --- | --- | --- | | A gallery in rows, columns, or masonry | `` | On by default | | One photo, such as a cover | `` | Add `lightbox` | | Several sections with one lightbox order | `` around albums/photos | On by default | | Horizontal browsing with thumbnails | `` | Add `:lightbox="true"` | | Your own thumbnail layout or viewer | `` and primitives | You compose it | ```vue ``` Most apps need only `PhotoAlbum`. Change its look with props, CSS variables, and slots before you reach for the lower-level components. ## What you provide Each photo is a plain object. Four fields are required: ```ts const photo: PhotoItem = { id: 'moss-canyon', // stable and unique, not the array index src: '/photos/moss-canyon.jpg', width: 1280, // real pixel size of the image file height: 853, } ``` Nuxt Photo does not measure images in the browser, because that would delay the layout until every image has downloaded. Store the sizes when you upload or import images; [Use your own photos](/raw/docs/guides/use-your-photos.md) shows how. ## When to use something else - A plain CSS grid is enough when nobody needs a lightbox, zoom, or rows that fill the width. - Use a media viewer such as lightGallery when you need video or plugins. Nuxt Photo 1.0 shows photos only. [Compare gallery approaches](/raw/docs/start/compare-approaches.md) covers PhotoSwipe, lightGallery, Nuxt Image, and a custom stack. --- Source: https://nuxt-photo.lupinum.com/docs/start/get-started --- title: "Get started" description: "Install the Nuxt module, paste a complete album page, and check the lightbox. Then replace the sample photos with your own." url: "https://nuxt-photo.lupinum.com/docs/start/get-started" route: "/docs/start/get-started" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- > Install the Nuxt module, paste a complete album page, and check the lightbox. Then replace the sample photos with your own. In five minutes you have a styled album with a working lightbox. These docs describe the Nuxt Photo 1.0 beta. Install the `next` tag. You need Nuxt **4.4.8** or later in Nuxt 4, and Node **22.18+** or **24.11+**. ## 1. Install the module ```bash [Terminal] pnpm add @lupinum/nuxt-photo@next # or: yarn add @lupinum/nuxt-photo@next # or: npm install @lupinum/nuxt-photo@next # or: bun add @lupinum/nuxt-photo@next ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], nuxtPhoto: { css: 'all', }, }) ``` The module registers `Photo`, `PhotoAlbum`, `PhotoGroup`, and `PhotoCarousel`, and auto-imports helpers such as `responsive` and `useLightbox`. `css: 'all'` adds the complete look. Without it you get only the structure CSS, for apps that bring their own theme. ## 2. Add an album page ```vue [app/pages/gallery.vue] ``` The sample URLs contain their sizes, so the page works before you add files. ## 3. Check the result Start the dev server and open `/gallery`: - The three photos fill the width in justified rows, with no jump while they load. - Selecting a photo opens the lightbox from the thumbnail. - `ArrowLeft` and `ArrowRight` change the photo; a click zooms to real pixels. - `Escape` closes the lightbox and focus returns to the thumbnail. ## 4. Use your own photos Replace the array with your data. Each photo needs: | Field | Rule | | --- | --- | | `id` | Stable and unique in the collection. Not the array index. | | `src` | Image URL or public path. | | `width` | Real pixel width of the image file, not its displayed size. | | `height` | Real pixel height of the image file. | Add `alt` for every image that is not decorative. `caption` and `description` appear in the lightbox. Wrong `width` or `height` values still render, but the layout and the opening animation are wrong. Read the sizes from the file, your CMS, or your upload pipeline. Data from a CMS or an API: [Use your own photos](/raw/docs/guides/use-your-photos.md). Resized and modern image formats: [Deliver images](/raw/docs/guides/deliver-images.md). Questions? Ask in the [Lupinum OSS Discord](https://discord.lupinum.com). --- Source: https://nuxt-photo.lupinum.com/docs/start/examples --- title: "Examples" description: "Complete, copyable components that run on the page - portfolio grid with captions and crossfade, photos in an article, product carousel, Tailwind lightbox, custom caption and counter, and a custom viewer." url: "https://nuxt-photo.lupinum.com/docs/start/examples" route: "/docs/start/examples" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Examples > Complete, copyable components that run on the page - portfolio grid with captions and crossfade, photos in an article, product carousel, Tailwind lightbox, custom caption and counter, and a custom viewer. Each example runs on this page with the real package. The code under it is the same file, so you can copy it unchanged. Every example takes a `photos` prop: pass your own `PhotoItem[]`. ## Portfolio grid Columns with a caption under each photo. The column count follows the container width, and the lightbox crossfades between photos. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/PortfolioGrid.vue] ``` ## Photos in an article A lead photo and an album share one `PhotoGroup`, so the lightbox moves through all photos of the article in one order. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/ArticlePhotos.vue] ``` ## Product carousel A carousel with thumbnails next to the product details. Select the large photo to open the lightbox. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/ProductGallery.vue] ``` ## Custom caption and counter The built-in lightbox with its own counter, a two-line caption, and a wider mat around the photo. Complete example in 2 files. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/CaptionGallery.vue] ``` ```vue [app/components/CaptionLightbox.vue] ``` ## Tailwind lightbox A lightbox built from the lower-level components and styled only with Tailwind utilities. Your app needs Tailwind; see [Style and theme](/raw/docs/guides/style-and-theme.md#own-the-look-with-tailwind). Complete example in 2 files. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/TailwindGallery.vue] ``` ```vue [app/components/TailwindLightbox.vue] ``` ## Custom viewer in plain CSS Your own markup and CSS. Nuxt Photo still handles gestures, zoom, keyboard, and focus. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/CustomPhotoViewer.vue] ``` --- Source: https://nuxt-photo.lupinum.com/docs/start/ai-agents --- title: "AI coding agents" description: "Give Claude Code, Codex, Cursor, and other agents the Nuxt Photo docs for the installed version through one AGENTS.md pointer, plus llms.txt, Markdown pages, and the docs MCP server." url: "https://nuxt-photo.lupinum.com/docs/start/ai-agents" route: "/docs/start/ai-agents" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # AI coding agents > Give Claude Code, Codex, Cursor, and other agents the Nuxt Photo docs for the installed version through one AGENTS.md pointer, plus llms.txt, Markdown pages, and the docs MCP server. ## Agent setup `@lupinum/nuxt-photo` ships its documentation inside the package for coding agents such as Claude Code, Codex and Cursor. The pages match the installed version, so an agent does not follow the website for a different version. In a plain Vue app, use `@lupinum/vue-photo` in place of `@lupinum/nuxt-photo` below. To set it up, give your coding agent this task: ```text Set up @lupinum/nuxt-photo for coding agents as described under "Agent setup" in its README. ``` ### Steps for the agent 1. Find `node_modules/@lupinum/nuxt-photo/dist/agent/AGENTS.md`. Look in the directory of the project that depends on `@lupinum/nuxt-photo` first, then in its parent directories up to the repository root (workspaces can hoist packages). Read it; it lists the documentation pages. 2. Add the section below to the project's agent instructions: `AGENTS.md`, or `CLAUDE.md` if the project has only that file. If it has neither, create `AGENTS.md`. Write the path relative to the repository root, through `node_modules/@lupinum/nuxt-photo` (for example `apps/web/node_modules/@lupinum/nuxt-photo/...` in a workspace). Never write a resolved path such as `node_modules/.pnpm/...`: it contains the version and breaks after an upgrade. If a section for `@lupinum/nuxt-photo` already exists, leave it as it is. Use the path you found in place of the sample path: ```md ## @lupinum/nuxt-photo Before you change code that uses @lupinum/nuxt-photo, read `node_modules/@lupinum/nuxt-photo/dist/agent/AGENTS.md` and the pages it lists. They document the installed version. Prefer them over what you remember about this package and over the website. ``` 3. Do not copy the documentation into the project and do not install a skill. The section points into the installed package, so it stays correct after every upgrade or downgrade. If the file does not exist, the installed version has no packaged documentation. Read the package README and its TypeScript types instead. ## Read the docs before installing Every page also has a Markdown version on the site, for an agent that evaluates Nuxt Photo before it is installed. The complete examples on the site become code blocks there. | Source | Use it for | | --- | --- | | [`/llms.txt`](https://nuxt-photo.lupinum.com/llms.txt) | An index of every page with a description. | | `/raw/docs/.md` | One page as Markdown. | | [`/llms-full.txt`](https://nuxt-photo.lupinum.com/llms-full.txt) | All pages in one file, for a large context. | For example, this page is at `https://nuxt-photo.lupinum.com/raw/docs/start/ai-agents.md`. ## Connect the MCP server The docs site runs an MCP server with two read-only tools: `list-pages` and `get-page`. Add it to an agent that supports remote MCP servers: ```json [.mcp.json] { "mcpServers": { "nuxt-photo-docs": { "type": "http", "url": "https://nuxt-photo.lupinum.com/mcp" } } } ``` ## Let the types answer first An agent often reads the installed package before any docs. Every public prop and `PhotoItem` field has a description in the types, with its default and whether it can change after mount. Error messages name the fix and link to the page that explains it. Ask the agent to run your typecheck after a change: most wrong props and missing fields show up there. --- Source: https://nuxt-photo.lupinum.com/docs/start/plain-vue --- title: "Use Nuxt Photo with Vue" description: "Use the components in a plain Vue 3 app with Vite - install @lupinum/vue-photo, import components and the stylesheet, set labels, lightbox and image provider once with createPhoto, and deliver images without Nuxt Image." url: "https://nuxt-photo.lupinum.com/docs/start/plain-vue" route: "/docs/start/plain-vue" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- > Use the components in a plain Vue 3 app with Vite - install @lupinum/vue-photo, import components and the stylesheet, set labels, lightbox and image provider once with createPhoto, and deliver images without Nuxt Image. Plain Vue applications use `@lupinum/vue-photo`. They do not install or register the Nuxt module. These docs describe the 1.0 beta. Install the `next` tag. ## Install ```bash [Terminal] pnpm add @lupinum/vue-photo@next # or: yarn add @lupinum/vue-photo@next # or: npm install @lupinum/vue-photo@next # or: bun add @lupinum/vue-photo@next ``` ## Render the first album Import components and types from the Vue package root. Import the complete stylesheet once from the application entry or root component. ```vue [src/App.vue] ``` The components work without setup. Import every component, composable, type, and helper from `@lupinum/vue-photo`. The rest of these docs uses Nuxt auto-imports; in Vue, import the same names from the package root. ## Set options for the whole app `createPhoto` sets labels, lightbox options, validation and the image provider once: ```ts [src/main.ts] import { createApp } from 'vue' import { createPhoto } from '@lupinum/vue-photo' import App from './App.vue' import { cdnProvider } from './cdn-provider' createApp(App) .use( createPhoto({ labels: 'de', lightbox: { tools: ['download', 'fullscreen'] }, provider: cdnProvider, }), ) .mount('#app') ``` A component's own props win over these values. A wrong option throws a `TypeError` when the app starts. Without `labels`, the language comes from ``, with English as the fallback. When you render on the server, set `labels` explicitly, so server and browser use the same language. ## Deliver images Nuxt Image works only with the Nuxt module. Without a provider, each image uses the photo's own fields: `thumbSrc` or `src` for thumbnails, `src` for slides, and `srcset` when you set it. The layout writes `sizes`. For resized images from your CDN, write a [provider](/raw/docs/guides/integrate-a-custom-image-service.md): Nuxt Photo then builds `srcset` from 128 to 2560 px, never wider than the photo. --- Source: https://nuxt-photo.lupinum.com/docs/start/compare-approaches --- title: "Compare gallery approaches" description: "Nuxt Photo compared with PhotoSwipe, lightGallery, Nuxt Image, and a custom stack - album layout, lightbox, carousel, image delivery, video, and licensing." url: "https://nuxt-photo.lupinum.com/docs/start/compare-approaches" route: "/docs/start/compare-approaches" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Compare gallery approaches > Nuxt Photo compared with PhotoSwipe, lightGallery, Nuxt Image, and a custom stack - album layout, lightbox, carousel, image delivery, video, and licensing. Choose by the work your app should own: albums and a lightbox together, only a viewer, a media gallery with video, only image delivery, or everything. Checked on **2026-07-14** against each project's official documentation. | Choice | Primary job | Album layout | Lightbox | Carousel | Nuxt image delivery | Application-owned integration | | --- | --- | --- | --- | --- | --- | --- | | Nuxt Photo | Coordinated Nuxt photo UI | Rows, columns, masonry | Built in | Built in | Optional integration | Low for ready-made components | | PhotoSwipe | Focused image viewer | Application-owned | Built in | Application-owned | Application-owned | Medium | | lightGallery | Plugin-rich image and video viewer | Application-owned | Built in | Viewer carousel | Application-owned | Medium | | Nuxt Image | Responsive image delivery | None | None | None | Primary capability | High for gallery behavior | | DIY Nuxt stack | Application-specific system | Your choice | Your choice | Your choice | Your choice | Highest | ## Choose Nuxt Photo when - You want albums, grouped navigation, carousel, lightbox, gestures, and SSR behavior to share one photo model. - You have stable IDs and real image dimensions. - The ready-made components cover most of the design. - You want Nuxt Image integration without making it a requirement. The trade-off is intentional scope. Nuxt Photo does not expose every Embla option, media plugin, or image-provider detail. ## Choose PhotoSwipe when PhotoSwipe is a focused viewer with progressive-enhancement markup, responsive image support, zoom, and dynamic core loading. Its setup requires predefined image dimensions and leaves the visible gallery layout to your application. Choose it when the viewer is the product and you want to own thumbnail layout and Nuxt integration. See the [PhotoSwipe getting-started guide](https://photoswipe.com/getting-started/) and [Vue gallery example](https://photoswipe.com/vue-image-gallery/). ## Choose lightGallery when lightGallery provides a Vue component and a large plugin surface for thumbnails, video, zoom, autoplay, sharing, rotation, fullscreen, and more. That breadth is useful for mixed-media experiences. Review its GPL and commercial licensing before adoption. Choose it when plugin breadth and video support matter more than Nuxt-native SSR and album ownership. See the [Vue integration](https://www.lightgalleryjs.com/docs/vue-image-video-gallery/) and [official feature and licensing guide](https://www.lightgalleryjs.com/docs/getting-started/). ## Use Nuxt Image with Nuxt Photo Nuxt Image generates provider-backed responsive URLs and attributes. It does not own albums, navigation, modal behavior, or carousels. Nuxt Photo can use that delivery pipeline for thumbnails and lightbox slides. See the [Nuxt Image configuration reference](https://image.nuxt.com/get-started/configuration/). ## Build a custom stack when Choose a custom stack when the application has requirements outside Nuxt Photo's supported model and the team is prepared to own: - Collection identity and navigation order - Layout measurement and SSR fallbacks - Modal focus and page isolation - Gesture conflicts and zoom state - Carousel and lightbox synchronization - Vendor upgrades and accessibility regression tests The custom path offers the most control and the highest ongoing integration cost. --- Source: https://nuxt-photo.lupinum.com/docs/start/performance-and-bundle-size --- title: "Performance and bundle size" description: "Measured client bundle size for the Nuxt module, PhotoAlbum, and single Vue imports, and how to keep image cost low." url: "https://nuxt-photo.lupinum.com/docs/start/performance-and-bundle-size" route: "/docs/start/performance-and-bundle-size" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Performance and bundle size > Measured client bundle size for the Nuxt module, PhotoAlbum, and single Vue imports, and how to keep image cost low. On a photo page the images cost far more than the script. Get image delivery right first, then check which components you ship. ## Keep image cost low - Store the real `width` and `height`, so the browser reserves space before each image loads. - Serve resized files: use Nuxt Image, or `srcset` candidates you prepare. - Set `sizes` to the width the thumbnail really takes, so the browser does not pick a file that is larger than needed. - Use `thumbSrc` when you have a separate small file for thumbnails. - Keep images below the first screen lazy (the default). [Deliver images](/raw/docs/guides/deliver-images.md) covers each option. ## Bundle size Production builds, Brotli-compressed. Nuxt rows are the difference from the same empty Nuxt app. Measured on 1 October 2026 with the 1.0 beta; the repository's `size` check keeps each value under its limit. | What the app uses | Brotli | | --- | --- | | Nuxt module enabled, no photo component rendered | `+1.83 kB` | | Nuxt module with one `PhotoImage` | `+1.96 kB` | | Nuxt module with one `PhotoAlbum` | `+33.5 kB` | | Vue: `responsive` only | `657 B` | | Vue: `useLightbox` only | `701 B` | | Vue: `PhotoImage` only | `1022 B` | | Vue: the complete package | `35.6 kB` | `PhotoAlbum` includes the layout, the lightbox, gestures, zoom, and image handling, so it is the number to compare with other gallery libraries. Nuxt auto-imports add only the components a page renders. Measure your own app too. Other modules, route splitting, CSS, and image formats change what users download. --- Source: https://nuxt-photo.lupinum.com/docs/guides/use-your-photos --- title: "Use your own photos" description: "Show a folder from public/ with usePhotoFolder, map CMS or API records to PhotoItem, get real image width and height at upload or from localImages, keep IDs stable, type custom meta data, and handle invalid records with validation=\"drop\"." url: "https://nuxt-photo.lupinum.com/docs/guides/use-your-photos" route: "/docs/guides/use-your-photos" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Use your own photos > Show a folder from public/ with usePhotoFolder, map CMS or API records to PhotoItem, get real image width and height at upload or from localImages, keep IDs stable, type custom meta data, and handle invalid records with validation="drop". Every component takes the same photo shape, `PhotoItem`. Convert your CMS or API records to it once, where the data enters your app, and pass the result to any component. ## Show a folder of images For images in `public/`, let Nuxt Photo read the folder. It reads each file's size at build time and makes a blurred preview, so you write no photo data: ```vue [app/pages/trips.vue] ``` - `usePhotoFolder` is async: `await` it in ` ``` `useFetch` loads the data on the server, so the album is laid out in the first HTML. When the records have no ID, use another value that is unique and does not change, such as the image URL without its size or quality parameters. Never use the array index: the lightbox and the thumbnails would lose their link when the order changes. ## Required and optional fields | Field | Required | Meaning | | --- | --- | --- | | `id` | Yes | Stable, unique string. Use the asset ID, never the array index. | | `src` | Yes | Image URL. The lightbox uses it. | | `width` | Yes* | Real pixel width of the image file, a positive number. | | `height` | Yes* | Real pixel height of the image file, a positive number. | | `alt` | No | Alternative text. Add it unless the image is decorative. | | `caption` | No | Short text under the photo in the lightbox. | | `description` | No | Longer text in the lightbox. | | `thumbSrc` | No | Another crop for thumbnails. It is resized like `src`. | | `srcset` | No | Your own fixed sizes. Used only without an image provider. | | `placeholderSrc` | No | Low-quality preview until the image loads. | | `meta` | No | Your own data, passed to slots and providers. | * Not needed for files in `public/` that come from `usePhotoFolder` or, with `localImages`, from a hand-written array. Without them, the error says so: `photo "lake" has no width; pass width and height, or in Nuxt use localImages / usePhotoFolder for files in public/`. `width` and `height` describe the original file, even when your image service returns a smaller version. Nuxt Photo uses them for the aspect ratio: the layout before the image loads and the frame of the lightbox animation. ## Get the image size at upload Nuxt Photo does not measure images in the browser, because that would delay the layout until every image has downloaded. Store the size with the asset. Most CMSs and image services already return it; use that value. When you handle uploads yourself, read the size on the server, for example with `sharp`: ```ts [server/utils/read-image-size.ts] import sharp from 'sharp' export async function readImageSize(file: Buffer) { const { width, height } = await sharp(file).metadata() if (!width || !height) throw new Error('The uploaded image has no readable size.') return { width, height } } ``` Call it before you save the upload, and store both values. For existing assets, run a one-time backfill on the server. Do not fetch URLs that users control without your normal server-side request protections. ## Keep your own data on the photo `meta` carries typed data to slots and providers: ```ts type Credit = { photographer: string } const photos = records.map((record) => ({ id: String(record.id), src: record.image.url, width: record.image.width, height: record.image.height, meta: { photographer: record.image.credit }, })) satisfies PhotoItem[] ``` ## Handle invalid records By default a component throws when a photo has no `id`, no `src`, a size that is not a positive number, or an `id` that is used twice. The error names the photo and the problem. Keep this default while you connect a new data source. When the page must render even with bad records, skip them and report them: ```vue ``` `validation="drop"` works on every component. A `Photo` with an invalid photo then renders nothing and emits `invalid-photos`. To check records yourself, for example before you store them, call `validatePhotos(records)`: it returns the valid `photos` and the `issues`, and never throws. ## Keep URLs stable during server rendering Create signed or expiring URLs before rendering and store the final URL on the photo. A URL computed from the current time or a random value differs between server and browser and causes a hydration mismatch. ## Check the result Test one portrait photo, one landscape photo, a record with a duplicate ID, and a record without a size. The first two keep their shape in the album; the last two stop with a clear error, or are skipped with `validation="drop"`. --- Source: https://nuxt-photo.lupinum.com/docs/guides/responsive-layouts --- title: "Responsive layouts and SSR" description: "Choose rows, columns, masonry, grid, bento, mosaic, or accordion, set row height, columns, and spacing per container width with responsive(), crop photos into tiles with a featured photo or a +N count, and prevent layout shift during SSR and hydration with defaultContainerWidth and breakpoints." url: "https://nuxt-photo.lupinum.com/docs/guides/responsive-layouts" route: "/docs/guides/responsive-layouts" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Responsive layouts and SSR > Choose rows, columns, masonry, grid, bento, mosaic, or accordion, set row height, columns, and spacing per container width with responsive(), crop photos into tiles with a featured photo or a +N count, and prevent layout shift during SSR and hydration with defaultContainerWidth and breakpoints. `PhotoAlbum` has seven layouts. Its numbers, such as the column count, can change with the width of the album's container, and the server can render the same layout that the browser will show. ## Choose a layout | Layout | What it does | Use it when | | --- | --- | --- | | `rows` | Photos share a row height and each row fills the width. Default. | Photos have mixed shapes. | | `columns` | Photos fill a fixed number of columns in reading order. Column widths vary so all columns end at the same height. | The column count and order matter. | | `masonry` | Equal-width columns. Each photo goes into the shortest column. | Density matters more than strict order. | | `grid` | Same-size tiles in equal columns. Photos are cropped. | Many photos must fit on one screen. | | `bento` | Big and small tiles on a grid without gaps. Photos are cropped. | A portfolio or project page. | | `mosaic` | A few photos fill one frame; the last tile shows how many more there are. | A card, a post, or a header. | | `accordion` | Slices share one strip; the slice under the pointer or focus opens. | A hero area with 3 to 8 photos. | ```vue ``` `targetRowHeight` defaults to `300` pixels: a larger value puts fewer photos in a row. `columns` defaults to `3`. `spacing` is the gap between photos (default `8`), and `padding` is space inside each photo item (default `0`). ## Crop photos into tiles `grid`, `bento`, `mosaic`, and `accordion` crop each photo to fill its tile, as CSS `object-fit: cover` does. CSS positions every tile, so the server HTML already has the final layout at every container width. These layouts do not need `defaultContainerWidth`. ```vue [app/pages/gallery.vue] ``` | Layout | Option | Default | Meaning | | --- | --- | --- | --- | | `grid` | `columns` | `3` | Columns. Accepts `responsive()`. | | `grid` | `aspectRatio` | `1` | Width divided by height of every tile. | | `bento` | `columns` | `4` | Columns. Accepts `responsive()`. | | `bento` | `aspectRatio` | `4 / 3` | Width divided by height of one grid cell. | | `bento` | `featured` | the first photo | `(photo, index) => boolean`: photos that get a big tile. | | `mosaic` | `aspectRatio` | `4 / 3` | Width divided by height of the whole frame. | | `mosaic` | `max` | `5` | Photos shown in the frame. | | `accordion` | `aspectRatio` | `2` | Width divided by height of the whole strip. | These rules can break the result: - **Bento and mosaic may move a photo.** Bento moves a photo at most four places to crop less and to leave no empty cells. Mosaic arranges its photos inside the frame. The page, the Tab key, and the lightbox all follow the order on screen. `open(index)` and the `index` in `featured` still use the position in `photos`. A parent `PhotoGroup` keeps its own navigation order. - **Bento chooses its order at the largest column count.** With `responsive()` columns, smaller counts keep that order and change only the tile shapes, so the order never changes with the width. - **Mosaic shows the first `max` photos.** The last tile shows `+N`. The lightbox still contains every photo and announces its position, such as "Slide 6 of 8". - **The accordion needs a pointer or a keyboard to open slices.** The first slice is open until another slice has the pointer or keyboard focus. On a touch screen, a tap opens the lightbox. - **Responsive values must be `responsive()` or have breakpoints.** CSS container queries apply `columns`, `spacing`, and `padding`. A plain function needs the `breakpoints` prop to know where to sample it; without it, the album uses the function's value at width `0`. ## Change values with the container width `responsive()` takes a value per minimum container width in pixels. Nuxt auto-imports it. ```vue [app/pages/gallery.vue] ``` The largest matching width wins: a 700-pixel album has three columns. The width is the album's container, not the browser window, so the same album works in a full page, a card, or a sidebar. `responsive()` works for `spacing`, `padding`, `targetRowHeight`, and `columns`. It cannot switch the layout type. For a smooth formula, pass a function of the container width instead: ```vue ``` Start with 4 pixels of spacing on narrow containers and 8 to 12 on wide ones. Use fewer columns when most photos are portraits or captions need room. ## Prevent a layout jump after server rendering `grid`, `bento`, `mosaic`, and `accordion` never jump: CSS places their tiles before any script runs. The other layouts compute positions from the width. The server does not know how wide the album will be. Give it the width the album usually has, and the first HTML already has the right rows or columns: ```vue ``` After the page loads, the album measures its container and uses the real width. Choose a value that is one of your `responsive()` keys and close to the common width, such as the width of your content column on desktop. Columns and masonry near the top of the page benefit most. When you use a function instead of `responsive()`, also list the widths the measurement should snap to: ```vue ``` `responsive()` adds its own keys to the breakpoints for you. The layout needs the real `width` and `height` of every photo. Wrong values still render, but photos then change their shape when they load. ## Check the result Open the page at a narrow, a medium, and a wide window, with the album inside its real parent. The column count and spacing change at your breakpoints. Reload with a slow network: the album keeps its shape while images load, and does not jump after hydration at the width you chose for `defaultContainerWidth`. For `grid`, `bento`, `mosaic`, and `accordion`, turn off JavaScript in the browser and reload: the tiles have the same size and place as with JavaScript. --- Source: https://nuxt-photo.lupinum.com/docs/guides/share-one-lightbox --- title: "Share one lightbox" description: "Let several albums and single photos on one page share one lightbox and one next/previous order with PhotoGroup. Collection rules, missing IDs, and turning the lightbox off." url: "https://nuxt-photo.lupinum.com/docs/guides/share-one-lightbox" route: "/docs/guides/share-one-lightbox" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Share one lightbox > Let several albums and single photos on one page share one lightbox and one next/previous order with PhotoGroup. Collection rules, missing IDs, and turning the lightbox off. Wrap the sections of a page in `PhotoGroup` and pass it every photo in the order the lightbox should follow. The albums and photos inside keep their own layout, and the arrow keys move through all of them. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/ArticlePhotos.vue] ``` ## Group albums and photos ```vue [app/pages/portfolio.vue] ``` Open the last summer photo and press `ArrowRight`: the first winter photo appears. ## The group's photos set the order `PhotoGroup` takes the order from its `photos` prop, not from the page. Moving the sections, showing one only on wide screens, or mounting one later does not change next and previous. To change the order, change the array. Each album or `Photo` inside connects its thumbnails to the group by photo `id`. So: - Every photo shown inside the group must also be in the group's `photos`. A missing ID throws an error that names it. - An `id` may appear only once in the group's `photos`. - A photo can be in the group's order without being shown, for example when a button opens it. - `lightbox-ignore` on a `Photo` shows a plain image that does not open the lightbox, while the photo stays in the order. The group owns the lightbox, so the `lightbox`, `transition`, and `navigation` props go on `PhotoGroup`, not on the albums inside it. ## Turn the lightbox off or on `lightbox` is read when the group mounts. To change it later, change the `key` so Vue mounts a new group: ```vue ``` ## Only one lightbox is open at a time Each group, album, or carousel with a lightbox has its own. Opening one closes any other, because only one can own focus and keyboard input. --- Source: https://nuxt-photo.lupinum.com/docs/guides/build-a-carousel --- title: "Build a carousel" description: "Horizontal photo browsing with PhotoCarousel - thumbnails, arrows, dots, loop, autoplay, several visible slides, right-to-left pages, custom controls, and an optional lightbox." url: "https://nuxt-photo.lupinum.com/docs/guides/build-a-carousel" route: "/docs/guides/build-a-carousel" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Build a carousel > Horizontal photo browsing with PhotoCarousel - thumbnails, arrows, dots, loop, autoplay, several visible slides, right-to-left pages, custom controls, and an optional lightbox. `PhotoCarousel` shows one photo at a time with arrows, a counter, and a thumbnail rail. Swipe or drag to browse. The lightbox is off until you turn it on. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/ProductGallery.vue] ``` ## Add a carousel ```vue [app/pages/print.vue] ``` `:lightbox="true"` makes a selected slide open in the lightbox. `lightbox` is read at mount; change the component `key` to turn it on or off later. ## Choose the controls `controls` lists what the carousel shows, in any order. The default is `['arrows', 'thumbnails', 'counter']`. | Control | Effect | | --- | --- | | `'arrows'` | Previous and next buttons. | | `'thumbnails'` | The thumbnail rail. | | `'counter'` | The "2 / 6" counter. | | `'dots'` | One dot per slide. | `loop` continues from the last slide to the first. ```vue ``` ## Show part of the next slide ```vue ``` `slideSize` sets the width of each slide. Each arrow press still moves one slide. `thumbSize` sets the thumbnail height; the width follows each photo. ## Play automatically ```vue ``` `autoplay: true` uses 4000 ms, stops after the first interaction, and keeps playing when the pointer is over the carousel. Write out all three fields when the behavior matters, so a later reader sees it. While autoplay runs, a pause button sits in the top corner, first in the tab order, and the counter stops announcing each slide to screen readers. Autoplay does not run for readers who prefer reduced motion, and starts when that preference is removed. ## Support right-to-left pages The carousel reads computed CSS direction from its root at mount. Set `dir` on an ancestor, or use CSS `direction` on the gallery root: ```vue
``` The key remounts the inline track when direction changes. Lightboxes read the gallery root direction again each time they open. ## Build your own controls The `controls` slot replaces the arrows and counter and gives you the actions: ```vue ``` Use native buttons and keep their visible focus. The [PhotoCarousel reference](/raw/docs/reference/photo-carousel.md#slots) lists every slot. Nuxt Photo does not expose other Embla options or plugins. ## Check the result Drag, use the arrows, and select a thumbnail: the counter and thumbnail follow. With `:lightbox="true"`, selecting the large photo opens the lightbox, and `Escape` returns focus to the carousel. --- Source: https://nuxt-photo.lupinum.com/docs/guides/deliver-images --- title: "Deliver images" description: "How Nuxt Photo sizes every thumbnail, slide and photo - srcset from your Nuxt Image screens, sizes from the layout, priority and preload for the first screen, build-time blur placeholders, browser cache headers, and the Nuxt Image config for Vercel, static sites, IPX and Cloudinary." url: "https://nuxt-photo.lupinum.com/docs/guides/deliver-images" route: "/docs/guides/deliver-images" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- > How Nuxt Photo sizes every thumbnail, slide and photo - srcset from your Nuxt Image screens, sizes from the layout, priority and preload for the first screen, build-time blur placeholders, browser cache headers, and the Nuxt Image config for Vercel, static sites, IPX and Cloudinary. Nuxt Photo asks your image service for the widths a photo can be shown at, and tells the browser how wide each photo is on screen. The browser then downloads one file close to the size it needs. With `@nuxt/image` installed this works without Nuxt Photo options: quality, format, widths and the provider all come from your `image` config. ## How a size is chosen Every image gets two attributes: | Attribute | Who writes it | What it says | | --- | --- | --- | | `srcset` | Nuxt Photo, from the widths your image service allows | The files that exist: `…?w=256 256w, …?w=640 640w, …` | | `sizes` | The component that knows the layout | How wide this image is on screen: `auto, 212px` | The browser multiplies the `sizes` width by the screen's pixel density and picks the smallest file that is large enough. Two rules keep this correct: - **No file is wider than the source.** A 1000 px photo never gets a `1280w` entry. The source width is the last entry when it falls between two widths. - **The layout owns `sizes`.** `PhotoAlbum` writes the exact width of each thumbnail from its layout, already in the server-rendered HTML. `PhotoCarousel` uses `slideSize` and the measured track. The lightbox uses the size of its frame. `` uses `auto` with a fallback. A `sizes` prop on `PhotoAlbum` or `PhotoImage` replaces the computed value. `sizes="auto, …"` lets browsers that support it use the measured width of a lazy image. Other browsers use the pixel value after it. ## Use Nuxt Image ```bash [Terminal] pnpm add @nuxt/image # or: yarn add @nuxt/image # or: npm install @nuxt/image # or: bun add @nuxt/image ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/image', '@lupinum/nuxt-photo'], image: { quality: 80, }, }) ``` Nuxt Photo detects `@nuxt/image` and uses its default provider. Module order does not matter. These Nuxt Image options apply to every Nuxt Photo image: | Nuxt Image option | Effect in Nuxt Photo | | --- | --- | | `provider` | Builds every URL. Choose another one for Nuxt Photo only with `nuxtPhoto.provider: 'cloudinary'`, or per component with `provider="cloudinary"`. | | `screens` × `densities` | The widths in `srcset`. With the defaults this is 640 to 3072 px. For IPX and other providers that accept any width, Nuxt Photo adds 128, 256, 384 and 512 px below that, for thumbnails. | | `quality` | Sent with every image. | | `format` | The first format is sent to IPX. Without a `format`, IPX gets `webp`. Vercel, Netlify and Cloudinary choose AVIF or WebP from the browser's `Accept` header instead. | | `domains` | Hosts of remote images, without `https://`. A remote image whose host is missing fails to load. | Animated GIFs never get a format, so they keep their animation. SVG files are passed through unchanged, with no `srcset`. ## Deploy on Vercel On Vercel, Nuxt Image uses the `vercel` provider by itself. Vercel only serves widths from a list that Nuxt Image builds from `image.screens` alone, not from screens × densities. Nuxt Photo uses exactly that list, so no width is rejected. The default screens run from 640 to 1536 px, so add small widths for thumbnails and large widths for 2× screens: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/image', '@lupinum/nuxt-photo'], image: { quality: 80, screens: { '3xs': 128, '2xs': 256, xs: 384, '3xl': 1920, '4xl': 2560, '5xl': 3072, }, }, }) ``` The default screens (`sm` to `2xl`) stay, because Nuxt Image merges this object with its defaults. Without the small screens, a 64 px thumbnail downloads a 640 px file. Without the large ones, a lightbox on a 2× laptop gets at most 1536 px. Vercel keeps each transformed image on its CDN. For files in `public/` this lasts up to 31 days, and a redeploy does not reset it. For remote images it lasts for the longer of the source's `Cache-Control: max-age` and `minimumCacheTTL`, which Nuxt Image sets to 300 seconds. When your CMS gives an edited image a new URL, raise it so visitors do not wait for a new transform every five minutes: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { vercel: { config: { images: { minimumCacheTTL: 2678400 } } }, }, }) ``` Use the `vercel` provider for server-rendered sites on Vercel. IPX inside a Vercel function cannot read `public/`, so local images return 404. ## Let browsers keep images Without a cache header, browsers ask the server again for every image on every visit. That costs a round trip per image while scrolling, even when the CDN answers from its cache. Two defaults cause it: | Where | Default | Effect | | --- | --- | --- | | Vercel, files in `public/` | `max-age=0, must-revalidate` | The optimized image inherits it. | | IPX | `image.ipx.maxAge: 60` | Browsers and CDNs keep a transformed image for one minute. | Give image folders a long cache time: ```ts [nuxt.config.ts] export default defineNuxtConfig({ routeRules: { '/photos/**': { headers: { 'cache-control': 'public, max-age=2592000, stale-while-revalidate=604800' }, }, }, image: { ipx: { maxAge: 2592000 }, }, }) ``` Vercel passes the source file's `max-age` on to the optimized image. Use 30 days: Vercel keeps an optimized image up to 31 days, and a shorter `max-age` makes browsers ask again once the cached copy is older than that. When you replace an image, give it a new file name, so browsers do not keep the old one. ## Generate a static site With `nuxi generate`, Nuxt Image uses `ipxStatic`: every width in every `srcset` is written to `.output/public` at build time, and no image is transformed when a visitor scrolls. The build takes longer with many photos. Thumbnails and slides that the server renders are included. A gallery that renders only in the browser (inside ``) is not; prerender its route or use a provider that transforms at request time. When IPX transforms at request time on your own server, keep `format` at `webp`. AVIF takes much longer to encode, and the first visitor waits for it. ## Use Cloudinary or another provider Any [Nuxt Image provider](https://image.nuxt.com/providers/introduction) works. Set it as the default provider, or only for Nuxt Photo: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/image', '@lupinum/nuxt-photo'], image: { cloudinary: { baseURL: 'https://res.cloudinary.com//image/upload/', }, }, nuxtPhoto: { provider: 'cloudinary', }, }) ``` Set each photo's `src` to the asset path after `baseURL`, with its original size: ```ts const photo: PhotoItem = { id: asset.public_id, src: `/${asset.public_id}.${asset.format}`, width: asset.width, height: asset.height, alt: asset.context?.custom?.alt, } ``` For an image service without a Nuxt Image provider, or in plain Vue, write a `PhotoProvider`: see [integrate a custom image service](/raw/docs/guides/integrate-a-custom-image-service.md). ## Show a placeholder while an image loads Nuxt Photo never shows an empty box. Each photo frame shows, in this order: 1. `placeholderSrc` from the photo, when you set it. 2. For files in `public/`: a blurred preview at most 32 px wide, made at build time and inlined as a data URL. Nuxt Photo makes it with `sharp`, which `@nuxt/image` already installs for IPX. Without `sharp` there is no preview, and the build logs one warning. Photos from [`usePhotoFolder`](/raw/docs/guides/use-your-photos.md) and, with `localImages`, hand-written arrays get it. 3. For remote images on IPX: a 24 px image from the provider. 4. Otherwise the background color `--np-placeholder-bg`. When an image fails to load, the placeholder stays, so the layout does not show an empty frame. ## Load the first screen first Images are lazy by default. Mark the images a visitor sees first, so the browser loads them before anything else: ```vue [app/pages/index.vue] ``` `priority` sets `loading="eager"` and `fetchpriority="high"`. In Nuxt it also adds a `` for the image to the page head, so the browser starts the download before it reads the rest of the page. At most six images get a preload. On `PhotoAlbum`, `priority` is the number of thumbnails, counted from the start. Set it to the number of thumbnails in the first screen; a higher number delays the rest of the page. The first slide of `PhotoCarousel` always has high priority. A `` cannot know its width before the page is shown, so it uses `100vw`. When the photo is narrower, say so, or the browser downloads a file for the full screen width: ```vue ``` Images below the first screen start loading about one and a half screen heights before they scroll into view, in every browser. The lightbox requests a slide only when it opens. After the open photo has loaded, it loads the previous and the next photo in the background, so the next swipe shows a ready image. With the browser's data saver on, it skips this. ## Check the result Open the network panel, disable the cache, and reload: - Thumbnails request files close to their displayed width times the screen's pixel density, never wider than the source. - With Nuxt Image, image URLs go through your provider: `/_ipx/…` for IPX, `/_vercel/image?…` on Vercel. On Vercel, a second load shows `x-vercel-cache: HIT`. - Images below the first screen are requested as you scroll toward them, and a placeholder is visible until each one is ready. - If local images work and remote ones fail, add the host to `image.domains`. --- Source: https://nuxt-photo.lupinum.com/docs/guides/integrate-a-custom-image-service --- title: "Integrate a custom image service" description: "Connect your own CDN, CMS image API, signed URLs or hand-built Cloudinary URLs with a PhotoProvider - a url function per width, optional placeholders, fixed renditions with srcset, a Nuxt Image custom provider for the whole app, per-component providers, and unit tests." url: "https://nuxt-photo.lupinum.com/docs/guides/integrate-a-custom-image-service" route: "/docs/guides/integrate-a-custom-image-service" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Integrate a custom image service > Connect your own CDN, CMS image API, signed URLs or hand-built Cloudinary URLs with a PhotoProvider - a url function per width, optional placeholders, fixed renditions with srcset, a Nuxt Image custom provider for the whole app, per-component providers, and unit tests. A provider turns a photo's `src` and a width into a URL. Nuxt Photo calls it once per width it offers, builds `srcset` from the results, and the layout writes `sizes`. You only describe how your service makes a URL. In Nuxt, check first whether a [Nuxt Image provider](https://image.nuxt.com/providers/introduction) exists for your service. Then [Deliver images](/raw/docs/guides/deliver-images.md) is all you need. ## Write the provider ```ts [app/utils/cdn-provider.ts] import { definePhotoProvider } from '@lupinum/nuxt-photo/app' const CDN = 'https://cdn.example.com/transform' export const cdnProvider = definePhotoProvider({ url(src, { width }) { return `${CDN}${src}?w=${width}&fmt=auto` }, }) ``` ```ts interface PhotoProvider { url(src: string, options: { width: number }): string placeholder?(src: string): string | undefined srcset?(photo: PhotoItem, context: 'thumb' | 'slide'): string | undefined } ``` - `url` gets `photo.src`, or `photo.thumbSrc` for thumbnails. It must accept any positive width: Nuxt Photo asks for 128 to 2560 px, never wider than the photo's `width`. - `placeholder` returns a small image URL to show while the photo loads, for example the same URL with `w=24`. - `srcset` is for services that only have fixed sizes. Return a complete `srcset` string, and Nuxt Photo uses it instead of calling `url` per width. - Let the service choose the format from the browser's `Accept` header (`fmt=auto`, `f_auto`). One `` cannot offer AVIF and WebP alternatives. ## Use it for one component ```vue ``` Every ready-made component and `PhotoImage` take the same prop. In Nuxt, `provider` also accepts a Nuxt Image provider name, such as `provider="cloudinary"`. ## Use it for the whole app In plain Vue, pass it to `createPhoto({ provider })`; see [use Nuxt Photo with Vue](/raw/docs/start/plain-vue.md). In Nuxt, register your service as a Nuxt Image custom provider, so `` and Nuxt Photo share it, and select it by name: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxt/image', '@lupinum/nuxt-photo'], image: { providers: { cdn: { provider: '~/providers/cdn.ts' }, }, }, nuxtPhoto: { provider: 'cdn' }, }) ``` Nuxt Image documents the provider file in [custom providers](https://image.nuxt.com/advanced/custom-provider). Nuxt Photo then takes widths, quality and format from your Nuxt Image config. ## Use fixed renditions When your CMS stores a few fixed sizes per image, return them as `srcset`: ```ts [app/utils/cms-provider.ts] import { definePhotoProvider, type PhotoItem } from '@lupinum/nuxt-photo/app' type Renditions = { renditions: { url: string; width: number }[] } export const cmsProvider = definePhotoProvider({ url: (src) => src, srcset(photo: PhotoItem) { const meta = photo.meta as Renditions | undefined return meta?.renditions.map((r) => `${r.url} ${r.width}w`).join(', ') }, }) ``` Put the renditions on the photo's `meta` when you map your records; see [use your own photos](/raw/docs/guides/use-your-photos.md). ## Keep signed URLs stable The provider runs on the server and again in the browser. It must return the same URL both times, or hydration fails. Do not call `Date.now()` or create random values in it. Sign URLs before rendering, in your API or data loader, store them on the photo, and return them from `url`. ## Build Cloudinary URLs without Nuxt Image ```ts [app/utils/cloudinary-provider.ts] import { definePhotoProvider } from '@lupinum/nuxt-photo/app' const CLOUD = '' export const cloudinaryProvider = definePhotoProvider({ url(src, { width }) { return `https://res.cloudinary.com/${CLOUD}/image/upload/c_limit,w_${width},q_auto,f_auto${src}` }, placeholder(src) { return `https://res.cloudinary.com/${CLOUD}/image/upload/w_24,q_30,e_blur:200,f_auto${src}` }, }) ``` Here `photo.src` is the public ID with a leading slash, such as `/trips/lake.jpg`. ## Test the provider A provider is a plain object. Test it without Vue: ```ts [test/cdn-provider.test.ts] import { expect, it } from 'vitest' import { cdnProvider } from '../app/utils/cdn-provider' it('asks the CDN for the requested width', () => { expect(cdnProvider.url('/trips/lake.jpg', { width: 640 })).toBe( 'https://cdn.example.com/transform/trips/lake.jpg?w=640&fmt=auto', ) }) ``` --- Source: https://nuxt-photo.lupinum.com/docs/guides/style-and-theme --- title: "Style and theme" description: "Change the look of albums and the lightbox - css all vs structure, CSS variables for colors, buttons, mat, glow and placeholders, ui classes, scoped styles, dark themes, and Tailwind CSS with a complete Tailwind lightbox." url: "https://nuxt-photo.lupinum.com/docs/guides/style-and-theme" route: "/docs/guides/style-and-theme" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Style and theme > Change the look of albums and the lightbox - css all vs structure, CSS variables for colors, buttons, mat, glow and placeholders, ui classes, scoped styles, dark themes, and Tailwind CSS with a complete Tailwind lightbox. Nuxt Photo splits its CSS in two. The **structure CSS** does the layout: row and column geometry, the lightbox position, gestures, and transitions. The **theme CSS** does the look: colors, buttons, shadows, and the glow. Keep the structure CSS and choose who owns the look. | `nuxtPhoto.css` | Loads | Use it when | | --- | --- | --- | | `'all'` | Structure and theme | You adjust the included look with CSS variables. | | `'structure'` | Structure only. Default. | Your own CSS or Tailwind owns the look. | | `'none'` | Nothing | You rewrite every structural rule. Rarely right. | With `'none'`, albums collapse and the lightbox cannot position photos until your CSS replaces all structural rules. ## Adjust the included look With `css: 'all'`, set CSS variables on `.np-lightbox`: ```css [app/assets/css/main.css] /* A light room, square photos, and no glow */ .np-lightbox { --np-backdrop-bg: #f4f2ee; --np-ambient-opacity: 0; --np-img-radius: 0; --np-btn-bg: rgba(255, 255, 255, 0.8); --np-btn-hover-bg: #fff; --np-btn-color: #1c1b19; --np-btn-border: rgba(0, 0, 0, 0.08); --np-focus-ring: #1c1b19; --np-counter-color: #4a4843; --np-caption-color: #1c1b19; --np-caption-secondary: #6b6862; } ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], css: ['~/assets/css/main.css'], nuxtPhoto: { css: 'all' }, }) ``` The mat around the photo is structure, so its variables work in both modes: ```css .np-lightbox { --np-frame-inset-inline: 72px; /* space left and right of the photo */ --np-caption-reserve: 120px; /* room below the photo for the caption */ } ``` The [CSS reference](/raw/docs/reference/css.md#css-variables) lists every variable and class. ## Match the placeholder to your page Before a photo loads, its frame shows the photo's blurred preview or average color. Photos without either show `--np-placeholder-bg`, a light gray. Set it for dark pages: ```css [app/assets/css/main.css] :root { --np-placeholder-bg: #e7e5e4; } .dark { --np-placeholder-bg: #292524; } ``` ## Add classes to albums and carousels Ready-made components take a `ui` object. Its classes are added to the component's own classes: ```vue ``` | Component | `ui` keys | | --- | --- | | `Photo` | `root`, `img`, `caption` | | `PhotoAlbum` | `root`, `item`, `img` | | `PhotoCarousel` | `root`, `slide`, `img`, `thumb`, `caption`, `controls` | In a ` ``` ## Own the look with Tailwind Load only the structure CSS, and add Tailwind: ```ts [nuxt.config.ts] import tailwindcss from '@tailwindcss/vite' export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], css: ['~/assets/css/main.css'], nuxtPhoto: { css: 'structure' }, vite: { plugins: [tailwindcss()] }, }) ``` ```css [app/assets/css/main.css] @import 'tailwindcss'; ``` Style albums with `ui`: ```vue ``` With `'structure'`, the built-in lightbox has no colors: the color variables above belong to the theme CSS and do nothing. Give it a look with your own rules on its classes: ```css [app/assets/css/main.css] @import 'tailwindcss'; .np-lightbox__backdrop { @apply bg-stone-950; } .np-lightbox__btn { @apply grid size-10 place-items-center rounded-full bg-white/10 text-white; @apply hover:bg-white/20 disabled:opacity-35; @apply focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white; } .np-lightbox__counter, .np-lightbox__label { @apply text-sm text-white/80; } ``` For full control over the markup, build the lightbox from the lower-level components. This one uses only Tailwind utilities. The mat comes from the `--np-frame-inset-*` variables, and the caption and arrows follow the photo through `--np-frame-*`: Complete example in 2 files. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/TailwindGallery.vue] ``` ```vue [app/components/TailwindLightbox.vue] ``` ## Check the result Open the lightbox in light and dark mode, and at phone width. Every button shows a visible focus ring with `Tab`, and disabled buttons look disabled. With `'structure'`, check that the backdrop is not transparent. --- Source: https://nuxt-photo.lupinum.com/docs/guides/customize-the-built-in-lightbox --- title: "Customize the lightbox" description: "Change the built-in lightbox without rebuilding it - download, share and full screen buttons, custom caption, counter, tool and action buttons with slots, slide overlays, fade or crossfade navigation, one lightbox for the whole app, and when to use lower-level components instead." url: "https://nuxt-photo.lupinum.com/docs/guides/customize-the-built-in-lightbox" route: "/docs/guides/customize-the-built-in-lightbox" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Customize the lightbox > Change the built-in lightbox without rebuilding it - download, share and full screen buttons, custom caption, counter, tool and action buttons with slots, slide overlays, fade or crossfade navigation, one lightbox for the whole app, and when to use lower-level components instead. Keep the built-in lightbox and replace only the parts you want. It still handles focus, keyboard, gestures, zoom, and the opening animation. By default it already shows each photo's `caption` and `description` under the photo, and a counter and buttons above it. Set those fields on your photos; you need a slot only to change how they look. ## Choose how far to go | You want to change | Use | | --- | --- | | Colors, mat, buttons | CSS variables: [Style and theme](/raw/docs/guides/style-and-theme.md) | | How photos change: slide, fade, crossfade | The `lightbox.navigation` option | | Caption, counter, action buttons, slide content | `Lightbox` slots, on this page | | Your own thumbnail layout | [A custom thumbnail layout](/raw/docs/guides/create-a-custom-thumbnail-layout.md) | | The whole lightbox markup | [Lightbox from primitives](/raw/docs/guides/build-a-lightbox-from-primitives.md) | Stay at the first level that works. A custom caption does not need a custom lightbox, and a custom thumbnail layout does not need a custom viewer. ## Fade or crossfade between photos ```vue ``` `slide` (default) moves photos on a strip, `fade` fades the current photo out and the next in, and `crossfade` fades the next photo in over the current one. Swiping works in every mode. The `lightbox` option is the same on `Photo`, `PhotoAlbum`, `PhotoGroup`, and `PhotoCarousel`. ## Add download, share and full screen buttons ```vue ``` The buttons appear before the zoom button, in the order you list them: | Tool | What it does | | --- | --- | | `download` | Downloads `photo.src`. For an image on another domain the browser ignores the download request, so the photo opens in a new tab. | | `share` | Opens the system share sheet with the photo's `alt` or `caption` and the page URL; with `deepLink`, the URL opens this photo. | | `fullscreen` | Shows the lightbox in full screen, and leaves it on a second press. | `share` and `fullscreen` are not rendered when the browser does not support them; iPhone Safari has no full screen for elements. Their labels are in every bundled language. To add your own button next to them, use the `tools` slot. It keeps the built-in tools, zoom and close: ```vue [app/components/LikeLightbox.vue] ``` ## Replace the caption and counter Wrap `Lightbox` in your own component, fill the slots you need, and pass the component to the `lightbox` prop: Complete example in 2 files. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/CaptionGallery.vue] ``` ```vue [app/components/CaptionLightbox.vue] ``` Slot props: | Slot | Props | | --- | --- | | `counter` | `{ activeIndex, count }` | | `caption` | `{ photo, index }` | | `tools` | `{ photo, index }` | | `actions` | `{ activePhoto, activeIndex, count, prev, next, close, toggleZoom, isZoomedIn, zoomAllowed, controlsDisabled }` | | `slide` | `{ photo, index, width, height }` | ## Replace the action buttons The `actions` slot replaces all buttons above the photo: tools, zoom and close. ```vue [app/components/AppLightbox.vue] ``` Use native buttons with an accessible name, and keep `disabled` and the visible focus style. ## Add an overlay to each slide The `slide` slot replaces the image area of each slide. Render `PhotoImage` to keep your image delivery: ```vue [app/components/CreditLightbox.vue] ``` ## Use one custom lightbox everywhere Pass the component per gallery: ```vue ... ``` Or set it once for the app, as a file path: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nuxtPhoto: { lightbox: { component: '~/components/CaptionLightbox.vue' }, }, }) ``` In plain Vue, pass the component itself: `app.use(createPhoto({ lightbox: { component: CaptionLightbox } }))`. A `component` set on a gallery still wins. The app-wide component does not turn a lightbox on: `Photo` and `PhotoCarousel` still need `lightbox`. Your component renders inside the provider that the gallery creates. Do not wrap it in another `LightboxProvider`, or it gets a second, empty state. ## Check the result Open a photo and move through several: your caption and counter follow the active photo. Press `Tab` through your buttons, then `Escape`: focus returns to the thumbnail of the photo you were viewing. --- Source: https://nuxt-photo.lupinum.com/docs/guides/create-a-custom-thumbnail-layout --- title: "Create a custom thumbnail layout" description: "Render thumbnails in any layout you build yourself - a grid, a hexagon grid, a list - and keep the built-in lightbox, keyboard access, and opening animation with LightboxProvider, PhotoTrigger, and PhotoImage." url: "https://nuxt-photo.lupinum.com/docs/guides/create-a-custom-thumbnail-layout" route: "/docs/guides/create-a-custom-thumbnail-layout" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Create a custom thumbnail layout > Render thumbnails in any layout you build yourself - a grid, a hexagon grid, a list - and keep the built-in lightbox, keyboard access, and opening animation with LightboxProvider, PhotoTrigger, and PhotoImage. When rows, columns, and masonry do not fit your design, lay out the thumbnails yourself. Three components keep the lightbox working: - `LightboxProvider` holds the photos and the lightbox state. - `PhotoTrigger` makes one thumbnail open its photo, with keyboard support and the opening animation. - `Lightbox` is the built-in viewer. ```vue [app/components/FeatureGrid.vue] ``` ## Rules - `index` is the photo's position in the provider's `photos`. It sets which photo opens and the lightbox order. - `hidden` is `true` while the opening or closing animation stands in for this thumbnail. Hide the thumbnail then, or the photo appears twice. - Render `PhotoImage` for the thumbnail, not a plain ``, so it uses your image delivery. - `PhotoTrigger` renders a button: click, `Enter`, and `Space` open the photo. ## Open a photo from a button in the layout A component inside the provider can control the lightbox with `useLightbox()`: ```vue [app/components/OpenFirstButton.vue] ``` Render it inside `LightboxProvider`. `useLightbox()` only finds a provider in a parent component, not in the same component. ## When PhotoGroup is the better choice When your layout is made of `PhotoAlbum` and `Photo` components, wrap them in [`PhotoGroup`](/raw/docs/guides/share-one-lightbox.md) instead. Use `LightboxProvider` when you render every thumbnail yourself. ## Check the result Tab to a thumbnail and press `Enter`: the lightbox opens from that thumbnail. Press `Escape`: focus returns to the thumbnail of the photo you were viewing. --- Source: https://nuxt-photo.lupinum.com/docs/guides/build-a-lightbox-from-primitives --- title: "Build a lightbox from primitives" description: "Write your own lightbox markup and CSS with LightboxRoot, LightboxViewport, LightboxSlide, LightboxControls, and LightboxCaption, while Nuxt Photo keeps state, gestures, zoom, focus, and the opening animation. Frame variables and an accessibility checklist." url: "https://nuxt-photo.lupinum.com/docs/guides/build-a-lightbox-from-primitives" route: "/docs/guides/build-a-lightbox-from-primitives" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Build a lightbox from primitives > Write your own lightbox markup and CSS with LightboxRoot, LightboxViewport, LightboxSlide, LightboxControls, and LightboxCaption, while Nuxt Photo keeps state, gestures, zoom, focus, and the opening animation. Frame variables and an accessibility checklist. Build the lightbox from the lower-level components when the built-in structure cannot match your design. You write the markup and CSS. Nuxt Photo still handles the photo state, navigation, gestures, zoom, the focus trap, and the opening animation. You own the final accessibility check. For a different caption, counter, or buttons, [the lightbox slots](/raw/docs/guides/customize-the-built-in-lightbox.md) are enough. ## A complete viewer `LightboxProvider` holds the photos. Each `PhotoTrigger` opens one. Inside `LightboxRoot`, the other components draw the backdrop, the slides, the controls, and the caption. This viewer uses plain CSS: Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/CustomPhotoViewer.vue] ``` ## Frame the photo and place the caption The lightbox fits each photo inside a frame area: the full-screen slide minus a mat on each side. Set the mat with three CSS variables on `LightboxRoot`. The frame area is measured inside `LightboxViewport`, so give the viewport a position such as `relative` or `absolute`. | Variable | Default | Space it keeps free | | --- | --- | --- | | `--np-frame-inset-top` | `clamp(40px, 7vh, 80px)` | Above the photo. | | `--np-frame-inset-bottom` | `clamp(40px, 7vh, 80px)` | Below the photo. | | `--np-frame-inset-inline` | `clamp(48px, 6vw, 120px)` | Left and right of it. | Below 700 pixels of screen width the defaults are 24 pixels above and below and 12 pixels at the sides. The defaults need the structure CSS. Without it, the photo uses the whole viewport. The lightbox then sets where the photo is drawn on `LightboxRoot`, in viewport pixels: `--np-frame-x`, `--np-frame-y`, `--np-frame-width`, and `--np-frame-height`. The viewer above uses them to put its caption directly under the photo. Keep full-size control layers click-through with `pointer-events: none`, and turn pointer events back on for the buttons inside. A click that reaches the viewport beside the photo closes the lightbox. ## Verify the custom structure Check the behavior that custom markup can affect: 1. Open every thumbnail with click, Enter, and Space. 2. Move through photos with the arrow, Home, and End keys. 3. Close with Escape and confirm focus moves to the thumbnail of the photo you were viewing. 4. Confirm the dialog has an accessible name. 5. Test touch swipe, zoom, and reduced motion. 6. Test the layout in left-to-right and right-to-left documents. Use [the primitives reference](/raw/docs/reference/primitives.md) for exact props and slot contracts. --- Source: https://nuxt-photo.lupinum.com/docs/guides/control-the-lightbox --- title: "Control the lightbox from code" description: "Open the lightbox at a given photo from a button, bind the active photo with v-model:active, close it with the browser Back button, link to a photo with deepLink, and move next and previous with template refs or useLightbox()." url: "https://nuxt-photo.lupinum.com/docs/guides/control-the-lightbox" route: "/docs/guides/control-the-lightbox" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Control the lightbox from code > Open the lightbox at a given photo from a button, bind the active photo with v-model:active, close it with the browser Back button, link to a photo with deepLink, and move next and previous with template refs or useLightbox(). There are two ways to control a lightbox from your code: - A **template ref** on any ready-made component opens and closes it. - **`useLightbox()`** in a child component also moves between photos and reads the current state. ## Open a photo from a button ```vue [app/components/GalleryWithButton.vue] ``` Indexes start at 0. `openById('canyon')` opens a photo by its `id`, which stays right when the order changes. Both reject with a `RangeError` for an index or ID that does not exist. Every ready-made component exposes the same handle: ```ts interface GalleryHandle { open(index?: number): Promise openById(id: string): Promise close(): Promise readonly isOpen: boolean readonly activeId: string | null readonly activePhoto: PhotoItem | null } ``` For an album inside a `PhotoGroup`, `open(index)` counts in that album; the group's handle counts in the group. `Photo` accepts index 0 or its own ID. `PhotoCarousel` also exposes `scrollTo(index)`, `next()`, and `prev()` for its inline track. ## Bind the active photo Use `v-model:active` on `Photo`, `PhotoAlbum`, `PhotoGroup`, or `PhotoCarousel`: ```vue [app/components/ControlledGallery.vue] ``` Enable the lightbox before using the model. `Photo` and `PhotoCarousel` need the `lightbox` prop; albums and groups enable it by default. Set an ID to open or navigate. Set `null` to close. User navigation updates the value. Unknown IDs are ignored with one development warning per component. The handle reports `activeId` and `activePhoto` as `null` when closed. For `Photo`, the model is its single ID or `null`. ## Control the lightbox from a child component `useLightbox()` returns the full controller of the nearest parent provider: `PhotoGroup` or `LightboxProvider`. ```vue [app/components/GalleryToolbar.vue] ``` ```vue [app/pages/gallery.vue] ``` Call `useLightbox()` in a component rendered inside the provider, not in the component that renders the provider. ```ts interface LightboxController { readonly photos: ComputedRef readonly count: ComputedRef readonly activeId: ComputedRef readonly activeIndex: ComputedRef readonly activePhoto: ComputedRef readonly isOpen: ComputedRef open(index?: number): Promise openById(id: string): Promise close(): Promise next(): void prev(): void toggleZoom(): void } ``` `next()` and `prev()` do nothing while the lightbox is closed. Calling `close()` twice is safe. The promises settle when the opening or closing animation has finished. ## Close the lightbox with the Back button The lightbox adds a browser history entry when it opens. Back closes the lightbox and leaves the page where it was: the page does not scroll, the router does not navigate, and route middleware does not run again. Closing with the close button or Escape removes the entry again, so a later Back leaves the page as usual. This is on by default. Turn it off for one gallery: ```vue ``` Or for the whole app: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nuxtPhoto: { lightbox: { history: false } }, }) ``` When several galleries are on one page, only the open lightbox writes history. ## Link to a photo With `deepLink`, the URL shows the open photo, so a shared link opens it: ```vue [app/pages/gallery.vue] ``` - While the lightbox is open, the URL has `?photo=`. Moving to another photo replaces the value; it does not add a history entry. - A page that loads with `?photo=` opens that photo without the opening animation, also after server rendering. - An ID that is not in this gallery is ignored. - `deepLink: 'image'` uses `?image=` instead. Give each gallery on a page its own name, so their links do not interfere. IDs appear in shared URLs. Use IDs that are stable and contain nothing private. Every lightbox option and its default is in the [lightbox reference](/raw/docs/reference/lightbox.md). --- Source: https://nuxt-photo.lupinum.com/docs/reference --- title: "Reference" description: "Where to look up exact names, props, defaults, events, slots, composables, types, module options, CSS variables, and package exports." url: "https://nuxt-photo.lupinum.com/docs/reference" route: "/docs/reference" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Reference > Where to look up exact names, props, defaults, events, slots, composables, types, module options, CSS variables, and package exports. Look up an exact name, type, default, event, slot, or import here. For a complete task, start with the [guides](/raw/docs/guides/use-your-photos.md). Your editor shows the same prop descriptions on hover, from the package types. ## Components - [`Photo`](/raw/docs/reference/photo.md): one photo, with an optional lightbox. - [`PhotoAlbum`](/raw/docs/reference/photo-album.md): rows, columns, or masonry with a lightbox. - [`PhotoGroup`](/raw/docs/reference/photo-group.md): one lightbox order across several sections. - [`PhotoCarousel`](/raw/docs/reference/photo-carousel.md): horizontal browsing with thumbnails. - [`Lightbox`](/raw/docs/reference/lightbox.md): the built-in viewer, its slots, keyboard, and gestures. - [Primitives](/raw/docs/reference/primitives.md): lower-level components for your own lightbox. ## APIs - [Composables](/raw/docs/reference/composables.md): lightbox state, labels, local folders, validation, providers, and `responsive`. - [Configuration and labels](/raw/docs/reference/configuration-and-labels.md): every `nuxtPhoto` option. - [Types](/raw/docs/reference/types.md): `PhotoItem`, providers, configuration, handles, controllers, and slot props. - [CSS](/raw/docs/reference/css.md): classes and CSS variables. - [Package exports](/raw/docs/reference/package-exports.md): the supported import paths. The package source and its type tests own these facts. The reference lists the public API available from the package entry points. --- Source: https://nuxt-photo.lupinum.com/docs/reference/photo --- title: "Photo" description: "The Photo component - props, slots, and behavior for one photo with an optional one-photo lightbox, alone or inside a PhotoGroup." url: "https://nuxt-photo.lupinum.com/docs/reference/photo" route: "/docs/reference/photo" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Photo > The Photo component - props, slots, and behavior for one photo with an optional one-photo lightbox, alone or inside a PhotoGroup. `Photo` renders one `PhotoItem` as a figure. It can remain a plain image, open a one-photo lightbox, or connect to a parent `PhotoGroup`. ## Minimal usage ```vue [app/pages/cover.vue] ``` ## Props `active` is optional (`string | null`, default `undefined`) and reactive. `v-model:active` sets the lightbox photo ID; `null` closes it. User navigation emits `update:active`. An unknown ID is ignored with a development warning. `lightbox` accepts `boolean | LightboxOptions`. Its enabled state and `component` are setup-time: remount with a new `key` to change them. The options inherit [configuration defaults](/raw/docs/reference/configuration-and-labels.md). `minZoom` is setup-time. Other lightbox options are read reactively; changing `deepLink` does not repeat the initial URL lookup. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `photo` | `PhotoItem` | - | **Required.** Photo to render. Uses the validation policy. | | `lightbox` | `boolean \| LightboxOptions` | `undefined` | Enable or replace a standalone lightbox. Ignored inside `PhotoGroup`. | | `lightboxIgnore` | `boolean` | `false` | Make this trigger inert inside `PhotoGroup` without changing the collection. | | `priority` | `boolean` | `false` | Load eagerly with high fetch priority. | | `sizes` | `string` | `'100vw'` | Width on screen as an HTML `sizes` value. Set it when the photo is not full width. | | `active` | `string \| null` | `undefined` | Lightbox photo ID; reactive. | | `provider` | `PhotoProvider \| string` | inherited | Image delivery; reactive. A name requires Nuxt Image. | | `ui` | `PhotoUi<'Photo'>` | `undefined` | Classes merged onto built-in classes; reactive. | | `validation` | `'throw' \| 'drop'` | inherited (`'throw'`) | Invalid photo policy; reactive. Dropping the photo renders no figure. | ## Events `Photo` emits `update:active` with its ID or `null`, and `invalidPhotos` with `InvalidPhotosEvent` when `validation="drop"` rejects its photo. ## Slots | Slot | Props | Purpose | | --- | --- | --- | | `slide` | `{ photo, index }` | Replace this photo's lightbox slide content. | ## Exposed template-ref API The exposed `GalleryHandle` has `open(index?: number): Promise`, `openById(id: string): Promise`, and `close(): Promise`. `open()` defaults to index `0`. Its readonly state is `isOpen: boolean`, `activeId: string | null`, and `activePhoto: PhotoItem | null`. `open()` accepts index `0`; `openById()` accepts this photo's ID. ## Important behavior - Without `lightbox` or a parent group, the figure is not interactive. - With `lightbox`, the photo owns a standalone one-item viewer. - Inside `PhotoGroup`, stable photo ID connects the trigger to the group's collection and navigation order. - `lightbox` is a setup-time option. Remount `Photo` with a new `key` to add, remove, or replace the standalone lightbox. - `photo`, `active`, `provider`, `validation`, `priority`, `sizes`, `ui`, and `lightboxIgnore` are reactive. A `priority` photo without `sizes` keeps `100vw` after the page loads, so its preload and image request the same file. ## Plain image inside a group ```vue ``` The second photo remains in programmatic group navigation, but its rendered thumbnail does not open the lightbox. [Pick a component](/raw/docs/start/introduction.md#pick-a-component) compares the ready-made components. [PhotoGroup](/raw/docs/reference/photo-group.md) defines shared collection behavior. ## Class targets and reactivity `ui` keys: `root`, `img`, `caption`. Collection data and class targets can change after mount. The `slide` slot receives `photo: PhotoItem` and a zero-based `index: number`. --- Source: https://nuxt-photo.lupinum.com/docs/reference/photo-album --- title: "PhotoAlbum" description: "The PhotoAlbum component - props, layout options, events, the thumbnail slot, and the template ref for rows, columns, masonry, grid, bento, mosaic, and accordion albums." url: "https://nuxt-photo.lupinum.com/docs/reference/photo-album" route: "/docs/reference/photo-album" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # PhotoAlbum > The PhotoAlbum component - props, layout options, events, the thumbnail slot, and the template ref for rows, columns, masonry, grid, bento, mosaic, and accordion albums. `PhotoAlbum` lays out one photo collection. Selecting a thumbnail opens the included lightbox unless the lightbox is disabled or owned by a parent group. ## Minimal usage ```vue [app/pages/gallery.vue] ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `photos` | `readonly PhotoItem[]` | - | **Required.** Ordered photos to render. | | `validation` | `'throw' \| 'drop'` | inherited (`'throw'`) | Invalid collection policy. | | `layout` | `AlbumLayout['type'] \| AlbumLayout` | `'rows'` | Layout type and its options. | | `spacing` | `ResponsiveParameter` | `8` | Gap between items in pixels. | | `padding` | `ResponsiveParameter` | `0` | Padding around each item in pixels. | | `defaultContainerWidth` | `number` | - | Assumed server-side container width. | | `breakpoints` | `readonly number[]` | derived | Width snap points; derived from `responsive()` when omitted. | | `sizes` | `string \| ResponsivePhotoSizes` | - | Replaces the `sizes` the layout writes. Usually leave it out. | | `priority` | `number` | `0` | Load the first N thumbnails first: eager, high fetch priority. | | `lightbox` | `boolean \| LightboxOptions` | `true` | Turn the lightbox off, or set its options. | | `active` | `string \| null` | `undefined` | Lightbox photo ID; reactive. | | `provider` | `PhotoProvider \| string` | inherited | Image delivery; reactive. A name requires Nuxt Image. | | `ui` | `PhotoUi<'PhotoAlbum'>` | `undefined` | Classes merged onto built-in classes; reactive. | Rows layout accepts `targetRowHeight` and defaults it to `300`. Columns, masonry, and grid accept `columns` and default it to `3`; bento defaults it to `4`. These values may use `ResponsiveParameter`. Grid, bento, mosaic, and accordion also accept `aspectRatio`; bento accepts `featured` and mosaic accepts `max`. See [crop photos into tiles](/raw/docs/guides/responsive-layouts.md#crop-photos-into-tiles) for their defaults. CSS places their tiles at every width, so they need no `defaultContainerWidth`; set it only to improve the server's `sizes` estimate. ### Lightbox and active photo - `lightbox` is read when the album mounts: turning it on or off, and `component`, need a new `key`. `minZoom` is also read once. The other options can change at any time; changing `deepLink` does not read the URL again. - Options you leave out come from the [configuration](/raw/docs/reference/configuration-and-labels.md). - `v-model:active` holds the open photo's ID, or `null` when the lightbox is closed. Setting it opens, moves or closes the lightbox. An unknown ID is ignored with a development warning. ## Events | Event | Payload | When it fires | | --- | --- | --- | | `invalidPhotos` | `InvalidPhotosEvent` | Invalid entries are found while `validation="drop"` is set. | | `update:active` | `string \| null` | Lightbox opens, navigates, or closes. | | `end-reached` | none | The end of the album comes within one screen height. Fires again only after `photos` grows. | ```vue [app/components/ValidatedAlbum.vue] ``` ## Slots | Slot | Props | Purpose | | --- | --- | --- | | `thumbnail` | `{ photo, index, width, height, hidden, sizes, priority }` | Replace each thumbnail content. | `index` is the position in `photos`, and `width` and `height` are the photo's own size, in every layout. `hidden` becomes `true` while the selected thumbnail is represented by the opening or closing animation. Pass `sizes` and `priority` to `PhotoImage` inside a custom thumbnail to keep the layout hints and configured provider. ## Exposed template-ref API The exposed `GalleryHandle` has `open(index?: number): Promise`, `openById(id: string): Promise`, and `close(): Promise`. `open()` defaults to index `0`. Its readonly state is `isOpen: boolean`, `activeId: string | null`, and `activePhoto: PhotoItem | null`. Inside `PhotoGroup`, a local index is translated to the photo ID. ## Important behavior - Rows produce justified photo rows. Columns keep a fixed column count. Masonry places each next photo in the shortest column. - Grid, bento, mosaic, and accordion crop photos to fill their tiles. CSS positions their tiles, so the server render is final at every width. - Bento and mosaic may move a photo a few places. The DOM, the Tab key, and the lightbox follow the order on screen; `open(index)` uses the position in `photos`. - Mosaic renders the first `max` photos. The lightbox contains all photos. - Layout options, photos, provider, validation, priority, ui, and responsive values are reactive. - `lightbox` is setup-time. Remount with a new `key` to add, remove, or replace it. - A parent `PhotoGroup` owns the lightbox and navigation collection. - Incorrect dimensions can produce incorrect layout and transition geometry. ## Responsive columns ```vue ``` Nuxt auto-imports `responsive()`. Plain Vue applications import it from `@lupinum/vue-photo`. Use [Responsive layouts and SSR](/raw/docs/guides/responsive-layouts.md) for layout decisions and [server rendering](/raw/docs/guides/responsive-layouts.md#prevent-a-layout-jump-after-server-rendering) for `defaultContainerWidth` and hydration behavior. ## Class targets and reactivity `ui` keys: `root`, `item`, `img`. All props other than lightbox capability, its component, and `minZoom` are reactive, except `defaultContainerWidth`, which seeds setup-time measurement. The thumbnail slot receives `photo: PhotoItem`, zero-based `index: number`, `width: number`, `height: number` (layout pixels), `hidden: boolean`, `sizes?: string`, and `priority?: boolean`. A missing dimension must resolve from configuration before layout; incorrect dimensions produce incorrect geometry. The end sentinel requires IntersectionObserver in the browser. --- Source: https://nuxt-photo.lupinum.com/docs/reference/photo-group --- title: "PhotoGroup" description: "The PhotoGroup component - props, events, slot, and template ref for one lightbox and navigation order across several albums and photos." url: "https://nuxt-photo.lupinum.com/docs/reference/photo-group" route: "/docs/reference/photo-group" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # PhotoGroup > The PhotoGroup component - props, events, slot, and template ref for one lightbox and navigation order across several albums and photos. `PhotoGroup` owns one ordered collection and lightbox. Descendant `Photo` and `PhotoAlbum` components register triggers by stable photo ID. ## Minimal usage ```vue [app/pages/portfolio.vue] ``` ## Props `active` is optional (`string | null`, default `undefined`) and reactive. `v-model:active` sets the lightbox photo ID; `null` closes it. User navigation emits `update:active`. An unknown ID is ignored with a development warning. `lightbox` accepts `boolean | LightboxOptions`. Its enabled state and `component` are setup-time: remount with a new `key` to change them. The options inherit [configuration defaults](/raw/docs/reference/configuration-and-labels.md). `minZoom` is setup-time. Other lightbox options are read reactively; changing `deepLink` does not repeat the initial URL lookup. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `photos` | `readonly PhotoItem[]` | - | **Required.** Canonical collection and order. | | `validation` | `'throw' \| 'drop'` | inherited (`'throw'`) | Invalid collection policy. | | `lightbox` | `boolean \| LightboxOptions` | `true` | Enable, disable, or replace the lightbox. | | `active` | `string \| null` | `undefined` | Lightbox photo ID; reactive. | | `provider` | `PhotoProvider \| string` | inherited | Image delivery; reactive. A name requires Nuxt Image. | | `ui` | `PhotoUi<'PhotoGroup'>` | `undefined` | Classes merged onto built-in classes; reactive. | ## Events | Event | Payload | When it fires | | --- | --- | --- | | `invalidPhotos` | `InvalidPhotosEvent` | Invalid entries are found while `validation="drop"` is set. | | `update:active` | `string \| null` | Lightbox opens, navigates, or closes. | ```vue ``` ## Slots | Slot | Props | Purpose | | --- | --- | --- | | `default` | `{ photos, controller }` | Render group content and access the full controller. | The slot `photos` value is readonly and keeps the canonical prop order. `controller` includes the `LightboxController` fields, plus `hiddenThumbnailIndex: Readonly>` and `setThumbnailRef(index: number)`, a callback that registers a thumbnail element. Child lightbox options are ignored with one development warning; the group owns the viewer. ## Exposed template-ref API The exposed `GalleryHandle` has `open(index?: number): Promise`, `openById(id: string): Promise`, and `close(): Promise`. `open()` defaults to index `0`. Its readonly state is `isOpen: boolean`, `activeId: string | null`, and `activePhoto: PhotoItem | null`. ## Important behavior - `photos` is the only identity and navigation-order source. - Every interactive descendant ID must exist in the collection. - A descendant becomes inert if its ID is removed while the descendant remains rendered. - Competing custom slide renderers for one ID throw during registration. - `photos`, `active`, `provider`, `validation`, and `ui` are reactive. - `lightbox` is setup-time. A group mounted with `:lightbox="false"` stays inert; remount with a new `key` to enable it. ## Several albums, one navigation order ```vue ``` Changing the visual order does not change next and previous navigation. Change the group `photos` array to change that order. [Share one lightbox](/raw/docs/guides/share-one-lightbox.md) explains the setup and why the collection is explicit. ## Class targets and reactivity `ui` keys: `root`. Collection data and class targets can change after mount. --- Source: https://nuxt-photo.lupinum.com/docs/reference/photo-carousel --- title: "PhotoCarousel" description: "The PhotoCarousel component - every prop, event, and slot for thumbnails, arrows, dots, loop, autoplay, controls, and the optional lightbox." url: "https://nuxt-photo.lupinum.com/docs/reference/photo-carousel" route: "/docs/reference/photo-carousel" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # PhotoCarousel > The PhotoCarousel component - every prop, event, and slot for thumbnails, arrows, dots, loop, autoplay, controls, and the optional lightbox. `PhotoCarousel` provides one slide per snap. Its lightbox is off by default. ## Minimal usage ```vue [app/pages/carousel.vue] ``` ## Props `active` is optional (`string | null`, default `undefined`) and reactive. `v-model:active` sets the lightbox photo ID; `null` closes it. User navigation emits `update:active`. An unknown ID is ignored with a development warning. It does not control the carousel's inline track. `lightbox` accepts `boolean | LightboxOptions`. Its enabled state and `component` are setup-time: remount with a new `key` to change them. The options inherit [configuration defaults](/raw/docs/reference/configuration-and-labels.md). `minZoom` is setup-time. Other lightbox options are read reactively; changing `deepLink` does not repeat the initial URL lookup. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `photos` | `readonly PhotoItem[]` | - | **Required.** Ordered slides. | | `validation` | `'throw' \| 'drop'` | inherited (`'throw'`) | Invalid collection policy. | | `loop` | `boolean` | `false` | Continue from the final slide to the first. | | `dragFree` | `boolean` | `false` | Allow the track to stop between snap points. | | `autoplay` | `boolean \| PhotoCarouselAutoplayOptions` | `false` | Enable autoplay. | | `slideSize` | `string` | - | CSS `flex-basis` for each slide. | | `slideAspect` | `string` | - | CSS `aspect-ratio` for each slide. | | `gap` | `string` | - | CSS gap between slides. | | `thumbSize` | `string` | - | Thumbnail height. The width follows the photo's aspect ratio. | | `lightbox` | `boolean \| LightboxOptions` | `false` | Enable or replace the optional lightbox. | | `active` | `string \| null` | `undefined` | Lightbox photo ID; reactive. | | `provider` | `PhotoProvider \| string` | inherited | Image delivery; reactive. A name requires Nuxt Image. | | `ui` | `PhotoUi<'PhotoCarousel'>` | `undefined` | Classes merged onto built-in classes; reactive. | | `controls` | `CarouselControl[]` | `['arrows', 'thumbnails', 'counter']` | Built-in controls to render; reactive. Values: `arrows`, `dots`, `counter`, `thumbnails`. | ## Events | Event | Payload | When it fires | | --- | --- | --- | | `invalidPhotos` | `InvalidPhotosEvent` | Invalid entries are found while `validation="drop"` is set. | | `update:active` | `string \| null` | Lightbox opens, navigates, or closes. | ```vue ``` ## Slots | Slot | Props | Purpose | | --- | --- | --- | | `slide` | `{ photo, index, selected, open }` | Replace each main slide. | | `thumb` | `{ photo, index, selected, goTo }` | Replace each thumbnail. | | `caption` | `{ photo, index, count }` | Replace the caption. | | `controls` | `{ goToPrev, goToNext, canGoToPrev, canGoToNext, selectedIndex, snapCount, goTo }` | Replace the complete control block. | | `prev` | - | Replace the previous-button content. | | `next` | - | Replace the next-button content. | | `dots` | `{ snaps, selectedIndex, goTo }` | Replace the dot indicators. | Custom `slide` and `thumb` slots replace provider-backed image content. Render `PhotoImage` inside the slot to keep the configured provider. ## Exposed template-ref API The exposed `GalleryHandle` has `open(index?: number): Promise`, `openById(id: string): Promise`, and `close(): Promise`. `open()` defaults to index `0`. Its readonly state is `isOpen: boolean`, `activeId: string | null`, and `activePhoto: PhotoItem | null`. The inline track also exposes `scrollTo(index: number): void`, `next(): void`, and `prev(): void`. These do not open the lightbox. ## Important behavior - The carousel uses documented Embla behavior internally, with one slide per snap. - `loop`, `dragFree`, visual options, and autoplay are reactive. - Direction comes from the gallery root's computed CSS at mount and at each lightbox open. Remount the inline track when its direction changes. - Main and thumbnail rails use the same direction. - `lightbox` is setup-time. Remount with a new `key` to add, remove, or replace it. - Arrow, thumbnail, dot, and pause buttons use the configured localization labels. - With autoplay, a pause and play button (`.np-carousel__autoplay`) comes first in the tab order. Autoplay does not run while the reader prefers reduced motion, and the counter's live region is silent while slides change on their own. ## Autoplay options ```ts interface PhotoCarouselAutoplayOptions { delayMs?: number stopOnInteraction?: boolean stopOnMouseEnter?: boolean } ``` `delayMs` defaults to `4000`, `stopOnInteraction` to `true`, and `stopOnMouseEnter` to `false`. `delayMs` must be a positive finite number. ```vue ``` [Build a carousel](/raw/docs/guides/build-a-carousel.md) covers practical setup. [Lightbox behavior](/raw/docs/reference/lightbox.md#keyboard-controls) lists keyboard and reduced-motion behavior. ## Class targets and reactivity `ui` keys: `root`, `slide`, `img`, `thumb`, `caption`, `controls`. Collection data and class targets can change after mount. Slot photo values are `PhotoItem`; indexes, counts, selectedIndex, and snapCount are numbers. `selected`, `canGoToPrev`, and `canGoToNext` are booleans. `open(): void | Promise` opens that slide's lightbox when enabled. `goTo(index: number): void`, `goToPrev(): void`, and `goToNext(): void` control the inline track. `snaps` is `readonly number[]`. The first inline slide has high image priority; other slides use lazy requests. Replacing the slide or thumb slot also replaces those built-in image hints. --- Source: https://nuxt-photo.lupinum.com/docs/reference/lightbox --- title: "Lightbox" description: "The built-in Lightbox component - slots, keyboard shortcuts, mouse and touch gestures, zoom, opening animation, slide/fade/crossfade navigation, reduced motion, and focus behavior." url: "https://nuxt-photo.lupinum.com/docs/reference/lightbox" route: "/docs/reference/lightbox" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Lightbox > The built-in Lightbox component - slots, keyboard shortcuts, mouse and touch gestures, zoom, opening animation, slide/fade/crossfade navigation, reduced motion, and focus behavior. `Lightbox` renders the ready-made viewer UI. It consumes a lightbox provider; it does not create a collection by itself. ## Minimal usage Wrap `Lightbox` and pass the wrapper to a ready-made collection component: ```vue [app/components/AppLightbox.vue] ``` ```vue [app/pages/gallery.vue] ``` The album creates the provider before it renders `AppLightbox`. ## Props `Lightbox` has no public props. Configure the collection component, provider, global defaults, and slots instead. ## Events `Lightbox` emits no public events. Use `useLightbox()` or the collection controller when application code needs open and close state. ## Slots | Slot | Props | Purpose | | --- | --- | --- | | `counter` | `{ activeIndex, count }` | Replace the slide counter. | | `actions` | `{ activeIndex, activePhoto, count, prev, next, close, toggleZoom, isZoomedIn, zoomAllowed, controlsDisabled }` | Replace the top action controls. | | `caption` | `{ photo, index }` | Replace the active photo caption. | | `tools` | `{ photo, index }` | Add content before built-in tools and Zoom. | | `slide` | `{ photo, index, width, height }` | Replace active slide image content. | ## Exposed template-ref API `Lightbox` exposes no public template-ref API. All ready-made components expose `GalleryHandle`. ## Important behavior - `Lightbox` must render inside a provider: a ready-made component's `lightbox` option or `LightboxProvider`. Elsewhere it throws an error that says so. - A slot replaces only the visible markup of its region. Keep native buttons, accessible names, disabled states, and visible focus in your markup. - Labels come from `nuxtPhoto.labels`; see [Configuration and labels](/raw/docs/reference/configuration-and-labels.md#labels). ## Focus and page isolation The lightbox is a modal dialog named "Photo viewer". While it is open, it takes keyboard input, moves focus into the viewer, and makes the page behind it inert. Closing returns focus to the thumbnail of the photo you were viewing. Only one lightbox is open at a time. ## Opening animation The lightbox grows the photo out of its thumbnail and shrinks it back on close. This is a FLIP animation (First, Last, Invert, Play): it measures the thumbnail and the final slide, then animates between them. Set it with the `lightbox.transition` option. | Mode | Behavior | | --- | --- | | `auto` | Uses FLIP when enough of the thumbnail is visible; otherwise fades. | | `flip` | Always tries to animate from the thumbnail. | | `fade` | Cross-fades without moving from the thumbnail. | | `none` | Opens without a transition. | `auto` is the default. `autoThreshold` defaults to `0.55`, which means at least 55 percent of the thumbnail must be visible. The visible part must also be at least 80 pixels wide and high; smaller thumbnails fade. Raise the threshold when movement from partly hidden thumbnails is distracting. Closing animates to the thumbnail of the photo you were viewing when it closed. It fades when that thumbnail is gone or outside the viewport. ## Changing photos The `lightbox.navigation` option sets how the lightbox moves from one photo to the next. | Mode | Behavior | | --- | --- | | `slide` | Photos sit on a strip. A swipe drags the strip with the finger. | | `fade` | The current photo fades out, then the next photo fades in. | | `crossfade` | The next photo fades in over the current one; the room never shows. | `slide` is the default. In `fade` and `crossfade`, a swipe still works: the photo follows the finger a little and fades with the distance. A long or fast swipe changes the photo, and a short one settles back. Complete example in one file. Use it as `` with your own `PhotoItem[]`. ```vue [app/components/NavigationModes.vue] ``` With reduced motion, `slide` jumps to the next photo and the fade modes change without a fade. ## Keyboard controls | Key | Action | | --- | --- | | `Escape` | Close and focus the thumbnail of the photo you were viewing. | | `ArrowRight` | Show the next photo, or pan when the current image is zoomed. | | `ArrowLeft` | Show the previous photo, or pan when zoomed. | | `Home` | Show the first photo. | | `End` | Show the last photo. | | `z` | Zoom to the photo's real pixels, or back to fit. | | `Tab` | Move through the lightbox controls, and show hidden controls. | Right-to-left direction reverses directional navigation. Text fields, text areas, selects, and editable content keep their keyboard input. Other controls receive the event before the lightbox. Call `event.preventDefault()` from a custom control when it must suppress a built-in shortcut such as Escape, an arrow key, Home, End, or `z`. ## Pointer gestures A click or tap beside the photo closes the lightbox. | Input | On the photo | While zoomed | | --- | --- | --- | | Mouse click | Zoom to the photo's real pixels. | Zoom back to fit. | | Touch tap | Show or hide the controls. | Show or hide them. | | Touch double tap | Zoom to the tapped point. | Zoom back to fit. | | Drag | Show the previous or next photo. | Move the photo. | | Vertical touch drag | Close the lightbox. | Move the photo. | | Pinch or mouse wheel | Change the zoom where supported. | Change the zoom. | The real-pixel ratio is the smaller of `photo.width / frameWidth` and `photo.height / frameHeight`, using the fitted frame's CSS pixels. Click, double tap, and `z` use that ratio capped at `4`, or `minZoom` if it is larger. This same value is the maximum zoom. `minZoom` can therefore enlarge a photo beyond its intrinsic pixels. Its default is `1.5`. Pan bounds keep a photo that is larger than the gesture area against that area's edges; a smaller photo stays centered instead of panning freely. Hidden controls leave the `Tab` order. A mouse movement or the `Tab` key shows them again. ## Reduced motion Nuxt Photo observes `prefers-reduced-motion`. It replaces movement-heavy transitions with a short fade and reacts when the preference changes. Custom lightbox content must provide its own reduced-motion treatment. Use native buttons for custom controls and keep visible focus styles. The [primitives guide](/raw/docs/guides/build-a-lightbox-from-primitives.md) has an accessibility checklist for custom markup. ## Tools, history, and URLs `lightbox.tools` defaults to `[]`. `download` links to `photo.src` with the `download` attribute. Cross-origin links open in a new tab with `rel="noopener"`. `share` uses the Web Share API with `title: photo.alt ?? photo.caption`. Its URL is the deep link when enabled, otherwise the current page URL. It appears only when the API is supported. `fullscreen` toggles the Fullscreen API on the lightbox root and appears only when it is supported. The `tools` slot receives `photo: PhotoItem | null` and `index: number`. Replacing `actions` also replaces the tools area. `lightbox.history` defaults to `true`. Opening pushes a browser history entry; Back closes the viewer. Closing a viewer opened from its own entry goes back. `deepLink` defaults to `false`; `true` uses the `photo` query parameter, and a string sets another parameter name. The value is the photo ID. Navigation updates the URL without a route navigation. An initial matching ID opens without the opening animation. An unknown ID does not open the viewer. Closing an initial deep link removes the parameter with `replaceState`. The built-in viewer mounts the active slide and one slide in each direction, wrapping around the collection, plus any slide still leaving. Other positions keep track spacers. The slot props use zero-based indexes. `actions.activePhoto` and `caption.photo` can be `null`; `slide.photo` is a `PhotoItem` and `width` and `height` are the fitted frame size in pixels. `counter.count` is the collection length. Action callbacks are `prev(): void`, `next(): void`, `close(): Promise`, and `toggleZoom(): void`. Its three control-state fields are booleans. `Lightbox` has no `ui` prop. --- Source: https://nuxt-photo.lupinum.com/docs/reference/primitives --- title: "Primitives" description: "Lower-level lightbox components for your own markup - LightboxProvider, LightboxRoot, LightboxOverlay, LightboxAmbient, LightboxViewport, LightboxSlide, LightboxControls, LightboxCaption, PhotoTrigger, PhotoImage." url: "https://nuxt-photo.lupinum.com/docs/reference/primitives" route: "/docs/reference/primitives" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Primitives > Lower-level lightbox components for your own markup - LightboxProvider, LightboxRoot, LightboxOverlay, LightboxAmbient, LightboxViewport, LightboxSlide, LightboxControls, LightboxCaption, PhotoTrigger, PhotoImage. Lower-level components share the nearest lightbox context. Import them from `@lupinum/nuxt-photo/app`. A ready-made component provides that context to its custom viewer. For a standalone composition, wrap triggers and viewer in `LightboxProvider`. ## `LightboxProvider` Renders its default slot without a wrapper. It provides context but does not render a viewer. It has no public events or exposed template-ref handle. | Prop | Type | Default | Timing and meaning | | --- | --- | --- | --- | | `photos` | `PhotoItem \| readonly PhotoItem[]` | required | Reactive collection. | | `transition` | `LightboxTransitionOption` | inherited (`'auto'`) | Reactive opening and closing animation. | | `navigation` | `LightboxNavigationMode` | inherited (`'slide'`) | Reactive photo change mode. | | `minZoom` | `number` | inherited (`1.5`) | Setup-time lowest click zoom. | | `component` | `Component` | inherited / unset | Accepted configuration key; this provider does not render it. | | `validation` | `InvalidPhotoPolicy` | inherited (`'throw'`) | Reactive invalid photo policy. | | `history` | `boolean` | inherited (`true`) | Read when entering or leaving history. | | `deepLink` | `boolean \| string` | inherited (disabled) | Initial URL read at setup; current value used for URL updates. | | `tools` | `LightboxTool[]` | inherited (no tools) | Reactive configuration for the built-in viewer. | ```vue [app/components/PhotoViewer.vue] ``` ## Viewer primitives These components emit no public events, expose no public template-ref handle, and have no `ui` prop. Normal attributes supply classes and accessible names. Unless listed below, they have no declared props. Their default slots have no slot props unless the table lists them. | Component | Function | Default slot props | | --- | --- | --- | | `LightboxRoot` | Teleports while open, traps focus, isolates the page, restores focus, and renders the opening animation layer. Supply `role="dialog"`, `aria-modal="true"`, and an accessible name. | none | | `LightboxOverlay` | Backdrop; a click on the overlay itself requests close. | none | | `LightboxAmbient` | Canvas glow of the current photo; reduced motion removes its crossfade. | none | | `LightboxViewport` | Gesture and wheel area. Attach `viewportRef` to the element that clips the slide track. | `photos: readonly PhotoItem[]`, `viewportRef: Ref`, `imageLoadFailed: boolean`, `isSlideMounted(index: number): boolean` | | `LightboxSlide` | Fitted photo frame with zoom and pan wrappers. | `photo: PhotoItem`, `index: number`, `width: number`, `height: number` (fitted pixels) | | `LightboxControls` | Controls wrapper and a polite slide announcement. | `LightboxControlsSlotProps` below | | `LightboxCaption` | Caption wrapper; supplies no text. | `photo: PhotoItem \| null`, `activeIndex: number` | `LightboxAmbient.duration?: number` defaults to `480` milliseconds and is reactive for subsequent fades. A remote image is requested with CORS first; without CORS headers it falls back to a stretched image that cannot be blurred from pixel data. ### `LightboxSlide` props | Prop | Type | Default | Timing | | --- | --- | --- | --- | | `photo` | `PhotoItem` | required | Reactive. | | `index` | `number` | required | Reactive collection position. | | `effectClass` | `string` | unset | Reactive classes for the effect wrapper. | | `frameClass` | `string` | unset | Reactive classes for the fitted frame. | | `zoomClass` | `string` | unset | Reactive classes for the zoom and pan wrapper. | The default slot replaces image content on the active or leaving slide. A recipe's custom slide renderer is used when there is no default slot. `isSlideMounted` covers the active photo and one photo in each direction, wrapping around the collection, plus leaving slides. Keep an equal-width spacer for an unmounted slide to preserve track positions. ### `LightboxControlsSlotProps` | Field | Type | | --- | --- | | `activeIndex`, `count` | `number` | | `activePhoto` | `PhotoItem \| null` | | `photos` | `readonly PhotoItem[]` | | `isZoomedIn`, `zoomAllowed`, `controlsDisabled` | `boolean` | | `next`, `prev`, `toggleZoom` | `() => void` | | `close` | `() => Promise` | ## `PhotoTrigger` Renders a native button. Click, Enter, and Space open its index. Its default slot receives `{ photo: PhotoItem, index: number, hidden: boolean }`. `hidden` is true while the animation represents the thumbnail. | Prop | Type | Default | Timing | | --- | --- | --- | --- | | `photo` | `PhotoItem` | required | Reactive photo and accessible label. | | `index` | `number` | required | Reactive position in the provider collection. | It has no public events, exposed handle, or `ui` prop. Its built-in class is `.np-trigger`. Do not nest another interactive element inside the button. ## `PhotoImage` Renders an image through the configured provider. It validates its photo strictly, including dimensions. A placeholder remains visible after a failed request. It has no slots, public events, exposed handle, or `ui` prop. Native image attributes and event listeners are forwarded to the image. | Prop | Type | Default | Timing and meaning | | --- | --- | --- | --- | | `photo` | `PhotoItem` | required | Reactive. | | `context` | `'thumb' \| 'slide'` | `'thumb'` | Reactive image context. | | `provider` | `PhotoProvider \| string` | inherited | Reactive; a name requires Nuxt Image. | | `priority` | `boolean` | `false` | Reactive; eager image with high fetch priority. | | `sizes` | `string` | `'100vw'` fallback | Reactive; layout hint, independent of provider. | A non-priority image is lazy and prefixes its sizes with `auto, ` unless they already start with `auto`. Pass the album thumbnail slot's `sizes` and `priority` to this component. --- Source: https://nuxt-photo.lupinum.com/docs/reference/composables --- title: "Composables" description: "Public helpers for lightbox state, reactive labels, async local folders, photo validation, providers, and responsive values." url: "https://nuxt-photo.lupinum.com/docs/reference/composables" route: "/docs/reference/composables" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Composables > Public helpers for lightbox state, reactive labels, async local folders, photo validation, providers, and responsive values. Nuxt auto-imports `useLightbox`, `usePhotoLabels`, `usePhotoFolder`, and `responsive` unless `autoImports` is disabled. All APIs below can be imported from `@lupinum/nuxt-photo/app`. `usePhotoFolder` is Nuxt-only. ## `useLightbox()` ```ts [useLightbox] function useLightbox< TMeta extends object = Readonly>, >(): LightboxController ``` Call during setup in a descendant of `LightboxProvider`, `PhotoGroup`, or a ready-made component's custom viewer. It throws without an active context. It returns readonly computed refs: `photos`, `count`, `activeId`, `activeIndex`, `activePhoto`, and `isOpen`. Its actions are `open(index?: number): Promise`, `openById(id: string): Promise`, `close(): Promise`, `next(): void`, `prev(): void`, and `toggleZoom(): void`. `open()` defaults to index `0`. Invalid indexes or IDs reject with `RangeError`. Use `LightboxProvider` to create a custom collection. ## `usePhotoLabels()` ```ts [usePhotoLabels] function usePhotoLabels(): PhotoLabels ``` Returns the complete labels as properties, rather than refs. Property reads follow the current locale and per-key overrides. Read them in templates or computed values to retain reactivity; copying a string stores its current value. See [configuration and labels](/raw/docs/reference/configuration-and-labels.md). ## `usePhotoFolder()` ```ts [usePhotoFolder] function usePhotoFolder( folder: string, options?: { alt?: Record caption?: Record sort?: 'name' | 'name-desc' }, ): Promise> ``` Await it during Nuxt setup. It returns a mutable, deeply reactive ref. `folder` is relative to `public/`; surrounding slashes are removed. Only files in that folder are included, without nested folders. IDs are paths relative to `public/`, without the extension. `alt` and `caption` maps use the filename without its extension, not the full path. Default sort is `name`, ordered by ID with a locale-independent string comparison; `name-desc` reverses it. ```vue [app/pages/gallery.vue] ``` The server uses the build-time public image manifest. Only the requested folder's records, including dimensions and available placeholders, transfer through the Nuxt payload; hydration reuses that payload. The payload key includes the normalized folder, sort, and sorted label map entries. This works with `localImages: false` and does not send the whole manifest to the client. A client request without cached payload fetches that folder from the server. An empty or unknown folder returns an empty array. Development warns once per folder when it is empty or contains photos without alt text. ## `validatePhotos()` ```ts [validatePhotos] function validatePhotos( input: unknown[], options?: { owner?: string }, ): { photos: (PhotoItem & { readonly width: number; readonly height: number })[] issues: PhotoValidationIssue[] } ``` Returns valid photos and issues without throwing or modifying the input. The default owner is `validatePhotos`. Invalid photos and all occurrences of a duplicate ID are dropped. It does not resolve missing dimensions from app configuration; supply dimensions before calling it. Returned photos have required numeric `width` and `height`. ## `definePhotoProvider()` ```ts [definePhotoProvider] function definePhotoProvider(provider: T): T ``` Returns the same object and preserves its inferred type. This helper does not validate at runtime. `url(src, { width, quality?, format? })` is required; `placeholder(src)` and `srcset(photo, context)` are optional. The provider owns URLs, not image `sizes`. See [PhotoProvider](/raw/docs/reference/types.md). ## `responsive()` ```ts [responsive] function responsive(breakpoints: Record): (containerWidth: number) => T ``` Keys are minimum container widths in pixels. The greatest matching key wins; below every key, the smallest key's value wins. The returned function carries breakpoint metadata used by `PhotoAlbum` for measurement snapping. ```ts [app/utils/gallery.ts] import { responsive } from '@lupinum/nuxt-photo/app' export const columns = responsive({ 0: 2, 640: 3, 1024: 4 }) ``` An empty map throws. Keys must be finite and non-negative; the input width must be finite. `PhotoAlbum` already measures its container. --- Source: https://nuxt-photo.lupinum.com/docs/reference/configuration-and-labels --- title: "Configuration and labels" description: "PhotoConfig, Nuxt module options, providers, lightbox defaults, precedence, ten locales, and label templates." url: "https://nuxt-photo.lupinum.com/docs/reference/configuration-and-labels" route: "/docs/reference/configuration-and-labels" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Configuration and labels > PhotoConfig, Nuxt module options, providers, lightbox defaults, precedence, ten locales, and label templates. Configure a Nuxt app with `nuxtPhoto` in `nuxt.config.ts`. Plain Vue apps install the `createPhoto(PhotoConfig)` plugin. ## Plain Vue configuration `createPhoto(config: PhotoConfig): Plugin` validates configuration before installation. Unknown keys and invalid values throw `TypeError`. All keys are optional. | Key | Type | Default | | --- | --- | --- | | `provider` | `PhotoProvider` | Native provider | | `labels` | `PhotoLocale \| Partial` | Detected locale | | `lightbox` | `LightboxOptions` | Defaults below | | `validation` | `'throw' \| 'drop'` | `'throw'` | | `dimensions` | `(src: string) => { width: number; height: number } \| undefined` | No resolver | Pass the plugin to `app.use(createPhoto(config))` before mounting. The `dimensions` resolver fills both dimensions only when both are missing or null. Explicit values are preserved; a partial or invalid dimension still fails validation. Label function keys accept functions in Vue configuration. ## Nuxt module options ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], nuxtPhoto: { css: 'all', labels: 'de', lightbox: { tools: ['download'], deepLink: true }, }, }) ``` | Key | Type | Default | Meaning | | --- | --- | --- | --- | | `css` | `'none' \| 'structure' \| 'all'` | `'structure'` | No CSS, layout CSS, or layout plus theme. | | `components` | `boolean \| { prefix?: string; primitives?: boolean }` | `{ prefix: '' }` | Register Photo, PhotoAlbum, PhotoGroup, PhotoCarousel. Primitives are off unless enabled. Lightbox stays import-only. | | `autoImports` | `boolean \| { prefix?: string }` | `true` | Register useLightbox, usePhotoLabels, usePhotoFolder, responsive. | | `localImages` | `boolean` | `false` | Send the public image dimension lookup to the client. | | `labels` | `PhotoLocale \| Partial>` | Detected locale | Locale or per-label strings and templates. | | `lightbox` | `LightboxOptions` | No module override | Shared viewer options below. | | `validation` | `'throw' \| 'drop'` | `'throw'` | Shared invalid photo policy. | | `provider` | `string` | Nuxt Image default provider, or native without Nuxt Image | Select a named Nuxt Image provider. Requires `@nuxt/image`. | Module options are build/setup configuration. `components: false` and `autoImports: false` disable registration. A component prefix `Np` produces `NpPhoto`, `NpPhotoAlbum`, `NpPhotoGroup`, and `NpPhotoCarousel`. An auto-import prefix `Np` produces `useNpLightbox`, `useNpPhotoLabels`, `useNpPhotoFolder`, and `npResponsive`. Explicit imports keep their original names. `localImages: true` resolves dimensions for hand-written local photo records. The build reads `public/`; development updates the lookup when files change. Remote images still need dimensions. A client manifest over 2,000 images produces a warning. `usePhotoFolder` works independently of this option and transfers only its requested folder. ## Lightbox options | Key | Type | Default | Timing and meaning | | --- | --- | --- | --- | | `component` | `Component` | Built-in Lightbox in enabled recipes | Setup-time viewer choice. | | `transition` | `'auto' \| 'flip' \| 'fade' \| 'none' \| { mode: 'auto' \| 'flip' \| 'fade' \| 'none'; autoThreshold?: number }` | `'auto'`, threshold `0.55` | Reactive opening and closing animation. | | `navigation` | `'slide' \| 'fade' \| 'crossfade'` | `'slide'` | Reactive photo change mode. | | `minZoom` | `number` | `1.5` | Setup-time lowest click zoom; positive finite number. | | `history` | `boolean` | `true` | Read on history operations; Back closes a viewer opened through its own entry. | | `deepLink` | `boolean \| string` | `false` | `true` uses query key `photo`; a string names the key. Initial URL lookup is setup-time. | | `tools` | `LightboxTool[]` | `[]` | Reactive list: `download`, `share`, `fullscreen`. | `Photo` and `PhotoCarousel` default to no viewer. `PhotoAlbum` and `PhotoGroup` default to an enabled viewer. Per-recipe `lightbox` accepts a boolean or these options; set `component` inside the options to replace the viewer. Use component objects in application component props. Nuxt module options are serialized to runtime configuration, so component objects belong in app code. ## Merge and precedence Built-in defaults are merged with app configuration, then the nearest group or provider, then component overrides. A component's `provider` and `validation` win over inherited values. Undefined values do not erase inherited values. `lightbox` options merge by key; `tools` is replaced as a whole. Per-label objects merge by key. A locale string replaces the inherited locale and clears inherited label overrides. A label object retains the inherited locale and overrides only defined keys. A `PhotoGroup` owns the viewer. Descendant recipe lightbox options are ignored with one development warning. Photo and image providers can still be overridden per component. Global configuration does not enable a disabled-by-default recipe. Set the recipe lightbox prop to `true` or an options object to enable its viewer. ## Image providers Without Nuxt Image the native provider keeps `src` and optional `srcset`. A native thumbnail uses `thumbSrc` when present and then omits the main photo's `srcset`. With Nuxt Image, `image.provider`, `image.quality`, `image.format`, `image.screens`, and `image.densities` are the source of image settings. A `nuxtPhoto.provider` name overrides the Nuxt Image provider name. IPX and ipxStatic default to WebP when no format is set. GIFs get no format modifier; SVGs pass through without a srcset. Pre-encoded local paths are decoded once before Nuxt Image. The default Vue width ladder is `[128, 256, 384, 512, 640, 828, 1080, 1280, 1640, 1920, 2560]`. Nuxt uses sorted unique screens multiplied by densities, with smaller default ladder widths below the first product. Vercel uses only sorted unique screen widths, without density multiplication or a terminal source-width candidate. Candidates never exceed source width. When allowed, the exact source width is added if the ladder reaches or passes it. Duplicate URLs become one candidate labeled with the larger width. A provider's `srcset()` supplies fixed renditions instead. Providers do not own `sizes`. Albums compute layout hints, carousels use the measured track and slide size, and lightbox slides use fitted frame width. `PhotoImage.sizes` and `PhotoAlbum.sizes` override their computed hints. Explicit `placeholderSrc` wins over provider previews. IPX remote previews use width `24` and quality `30`. Local folder records include build-time previews up to 32 pixels wide when Sharp is resolvable; otherwise the build warns once and omits those previews. ## Labels Supported locales: `en`, `de`, `fr`, `es`, `it`, `nl`, `pt`, `pt-PT`, `ar`, and `he`. `pt` is Brazilian Portuguese; `pt-PT` is European Portuguese. Plain Vue detects `` when no locale is configured; server rendering falls back to `en`. Nuxt follows the reactive locale from `@nuxtjs/i18n` when that module is installed; otherwise it uses `en`. Locale matching checks the full tag first, without case sensitivity, then the primary language. For example, `pt-pt` selects `pt-PT`, and `pt-BR` selects `pt`. Unsupported languages fall back to English. An explicit locale string fixes the label locale. Nuxt includes English and only configured or declared i18n locales in the build's label catalog. Arabic and Hebrew pages must set `dir="rtl"`. Direction comes from the page, not the language code. Nuxt label overrides are strings. `goToSlide` and `viewPhoto` replace every `{index}`; `slideStatus` replaces `{index}` and `{count}`. Index labels receive one-based numbers. Vue's corresponding `PhotoLabels` keys are functions: `(index: number) => string`, or `(index: number, count: number) => string` for `slideStatus`. All other keys are strings. | Label | Default | Used for | | --- | --- | --- | | `photoViewer` | `Photo viewer` | Lightbox accessible name | | `previous` | `Previous` | Previous photo control | | `next` | `Next` | Next photo control | | `zoom` | `Zoom` | Zoom control | | `fit` | `Fit` | Reset zoom control | | `close` | `Close` | Close control | | `loadFailed` | `Image could not be loaded.` | Image failure message | | `previousSlide` | `Previous slide` | Carousel previous control | | `nextSlide` | `Next slide` | Carousel next control | | `pauseAutoplay` | `Pause slideshow` | Carousel pause button | | `playAutoplay` | `Play slideshow` | Carousel play button | | `download` | `Download` | Download tool | | `share` | `Share` | Share tool | | `fullscreen` | `Full screen` | Enter fullscreen | | `exitFullscreen` | `Exit full screen` | Exit fullscreen | | `goToSlide` | `Go to slide {index}` | Carousel dot and thumbnail label | | `viewPhoto` | `View photo {index}` | Photo trigger label | | `slideStatus` | `Slide {index} of {count}` | Polite slide announcement | ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], nuxtPhoto: { labels: { close: 'Close viewer', slideStatus: 'Photo {index} of {count}', }, }, }) ``` --- Source: https://nuxt-photo.lupinum.com/docs/reference/types --- title: "Types" description: "The complete public types for photos, providers, configuration, gallery handles, controllers, layouts, validation, labels, UI classes, and slots." url: "https://nuxt-photo.lupinum.com/docs/reference/types" route: "/docs/reference/types" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Types > The complete public types for photos, providers, configuration, gallery handles, controllers, layouts, validation, labels, UI classes, and slots. Nuxt apps import these types from `@lupinum/nuxt-photo/app`. Plain Vue apps use `@lupinum/vue-photo`. These are the complete type-only exports shared by both entry points. The Nuxt module root additionally exports `NuxtPhotoOptions`. ```text [Public types] LightboxControlsSlotProps, LightboxCaptionSlotProps, LightboxSlideSlotProps, LightboxViewportSlotProps, CarouselSlideSlotProps, CarouselThumbSlotProps, CarouselCaptionSlotProps, CarouselControlsSlotProps, CarouselDotsSlotProps, PhotoItem, AlbumLayout, RowsAlbumLayout, ColumnsAlbumLayout, MasonryAlbumLayout, GridAlbumLayout, BentoAlbumLayout, MosaicAlbumLayout, AccordionAlbumLayout, PhotoCarouselAutoplayOptions, ResponsivePhotoSizes, LightboxTransitionOption, LightboxNavigationMode, ResponsiveParameter, InvalidPhotoPolicy, InvalidPhotosEvent, PhotoValidationIssue, PhotoValidationIssueCode, PhotoConfig, PhotoProvider, LightboxOptions, LightboxTool, PhotoLocale, GalleryHandle, PhotoUi, CarouselControl, LightboxController, PhotoLabels ``` ## Photo data and validation ```ts [PhotoItem] interface PhotoItem>> { readonly id: string readonly src: string readonly thumbSrc?: string readonly placeholderSrc?: string readonly width?: number readonly height?: number readonly alt?: string readonly caption?: string readonly description?: string readonly srcset?: string readonly meta?: Readonly } ``` `id` must be a stable non-empty string, unique within the collection. Dimensions are optional in the input type, but must resolve to positive finite numbers before rendering. Both may be omitted with a configured dimension resolver or Nuxt folder data. Explicit partial dimensions still fail. `thumbSrc` supplies a thumbnail URL; `src` supplies the lightbox image. `meta` retains application data. ```ts [Validation types] type PhotoValidationIssueCode = | 'missing-id' | 'missing-src' | 'invalid-width' | 'invalid-height' | 'duplicate-id' | 'invalid-item' | 'invalid-optional-field' | 'invalid-meta' type PhotoValidationIssue = { readonly code: PhotoValidationIssueCode readonly owner: string readonly index: number readonly id?: string readonly message: string } type InvalidPhotoPolicy = 'throw' | 'drop' type InvalidPhotosEvent = { readonly owner: string readonly issues: readonly PhotoValidationIssue[] readonly rawPhotos: readonly unknown[] } ``` `PhotoValidationError` is a runtime-exported class that extends `Error`, with readonly `owner: string` and `issues: readonly PhotoValidationIssue[]`. `InvalidPhotoPolicy` defaults to `throw` in components. `drop` skips invalid photos and every occurrence of duplicate IDs; recipes emit `invalidPhotos`. ## Configuration types ```ts [PhotoConfig] interface PhotoProvider { url(src: string, options: { width: number; quality?: number; format?: string }): string placeholder?(src: string): string | undefined srcset?(photo: PhotoItem, context: 'thumb' | 'slide'): string | undefined } type LightboxTool = 'download' | 'share' | 'fullscreen' interface LightboxOptions { component?: Component transition?: LightboxTransitionOption navigation?: LightboxNavigationMode minZoom?: number history?: boolean deepLink?: boolean | string tools?: LightboxTool[] } interface PhotoConfig { /** Image URL resolver. Provider candidate sizing is handled by the image pipeline. */ provider?: PhotoProvider /** Locale code, or per-key overrides on top of the detected locale. */ labels?: PhotoLocale | Partial lightbox?: LightboxOptions /** Throw on invalid data by default; collection recipes can opt into dropping invalid photos. */ validation?: InvalidPhotoPolicy /** Resolve missing intrinsic dimensions before validating photo data. */ dimensions?: (src: string) => { width: number; height: number } | undefined } ``` `PhotoConfig` is the input to `createPhoto`. `PhotoLocale` is `'en' | 'de' | 'fr' | 'es' | 'it' | 'nl' | 'pt' | 'pt-PT' | 'ar' | 'he'`, plus regional tags such as `pt-BR` that use a bundled primary language. The [configuration reference](/raw/docs/reference/configuration-and-labels.md) lists all `PhotoLabels` fields, defaults, merge rules, and timing. ### `PhotoProvider` `url` is required. It receives a source URL and the requested rendition width. `placeholder` optionally returns a preview URL. `srcset` optionally returns fixed candidates for the full photo and `thumb` or `slide` context. Providers do not supply `sizes`. ## Gallery and lightbox controllers ```ts [Controllers] interface GalleryHandle>> { open(index?: number): Promise openById(id: string): Promise close(): Promise readonly isOpen: boolean readonly activeId: string | null readonly activePhoto: PhotoItem | null } interface LightboxController>> { readonly photos: ComputedRef[]> readonly count: ComputedRef readonly activeId: ComputedRef readonly activeIndex: ComputedRef readonly activePhoto: ComputedRef | null> readonly isOpen: ComputedRef open(index?: number): Promise openById(id: string): Promise close(): Promise next(): void prev(): void toggleZoom(): void } ``` Every ready-made component exposes `GalleryHandle`. The handle's state is unwrapped for template refs and returns null photo state while closed. `PhotoCarousel` also exposes `scrollTo(index: number): void`, `next(): void`, and `prev(): void` for its inline track. `useLightbox` returns the controller, whose state fields are computed refs; active photo state may remain available while closed. ## Layout and responsive types ```ts [AlbumLayout] type RowsAlbumLayout = { type: 'rows' targetRowHeight?: ResponsiveParameter } type ColumnsAlbumLayout = { type: 'columns' columns?: ResponsiveParameter } type MasonryAlbumLayout = { type: 'masonry' columns?: ResponsiveParameter } type GridAlbumLayout = { type: 'grid' columns?: ResponsiveParameter // default 3 aspectRatio?: number // tile width / height, default 1 } type BentoAlbumLayout = { type: 'bento' columns?: ResponsiveParameter // default 4 aspectRatio?: number // cell width / height, default 4 / 3 featured?: (photo: PhotoItem, index: number) => boolean // default: the first photo } type MosaicAlbumLayout = { type: 'mosaic' aspectRatio?: number // frame width / height, default 4 / 3 max?: number // photos in the frame, default 5 } type AccordionAlbumLayout = { type: 'accordion' aspectRatio?: number // strip width / height, default 2 } type AlbumLayout = | RowsAlbumLayout | ColumnsAlbumLayout | MasonryAlbumLayout | GridAlbumLayout | BentoAlbumLayout | MosaicAlbumLayout | AccordionAlbumLayout type ResponsiveParameter = T | ((containerWidth: number) => T) type ResponsivePhotoSizes = { size: string sizes?: Array<{ viewport: string; size: string }> } ``` Rows accept `targetRowHeight` (default `300`); columns, masonry and grid accept `columns` (default `3`, bento `4`). TypeScript accepts only the options of the chosen `type`: `max` is valid only on `mosaic`, and `featured` only on `bento`, where `TMeta` types the photo it receives. `ResponsivePhotoSizes` describes sizes with viewport conditions. ## Animation and carousel types ```ts [Lightbox options] type LightboxTransitionOption = | 'flip' | 'fade' | 'auto' | 'none' | { mode: 'flip' | 'fade' | 'auto' | 'none'; autoThreshold?: number } type LightboxNavigationMode = 'slide' | 'fade' | 'crossfade' interface PhotoCarouselAutoplayOptions { readonly delayMs?: number readonly stopOnInteraction?: boolean readonly stopOnMouseEnter?: boolean } ``` Autoplay defaults: `4000` milliseconds, stop on interaction, and keep playing on mouse enter. The delay must be positive and finite. `CarouselControl` is `'arrows' | 'dots' | 'counter' | 'thumbnails'`. `PhotoUi` is a partial record of string classes. `T` selects a recipe: | Type argument | Keys | | --- | --- | | `'Photo'` | `root`, `img`, `caption` | | `'PhotoAlbum'` | `root`, `item`, `img` | | `'PhotoGroup'` | `root` | | `'PhotoCarousel'` | `root`, `slide`, `img`, `thumb`, `caption`, `controls` | Omitting `T` allows the union of all recipe keys. ## Public slot-prop types ```ts [Slot props] interface LightboxControlsSlotProps>> { activeIndex: number activePhoto: PhotoItem | null photos: readonly PhotoItem[] count: number isZoomedIn: boolean zoomAllowed: boolean controlsDisabled: boolean next: () => void prev: () => void close: () => Promise toggleZoom: () => void } interface LightboxCaptionSlotProps>> { photo: PhotoItem | null activeIndex: number } interface LightboxSlideSlotProps>> { photo: PhotoItem index: number width: number height: number } interface LightboxViewportSlotProps>> { photos: readonly PhotoItem[] viewportRef: Ref imageLoadFailed: boolean isSlideMounted: (index: number) => boolean } interface CarouselSlideSlotProps>> { photo: PhotoItem index: number selected: boolean open: () => void | Promise } interface CarouselThumbSlotProps>> { photo: PhotoItem index: number selected: boolean goTo: (i: number) => void } interface CarouselCaptionSlotProps>> { photo: PhotoItem index: number count: number } interface CarouselControlsSlotProps { goToPrev: () => void goToNext: () => void canGoToPrev: boolean canGoToNext: boolean selectedIndex: number snapCount: number goTo: (i: number) => void } interface CarouselDotsSlotProps { snaps: readonly number[] selectedIndex: number goTo: (i: number) => void } ``` Slot indexes are zero-based. `selected` describes the inline carousel selection. `LightboxSlideSlotProps.width` and `height` describe fitted frame pixels. See [Lightbox](/raw/docs/reference/lightbox.md) for its recipe slot names; they differ from the lower-level caption and controls slots. ## Nuxt module type ```ts [NuxtPhotoOptions] import type { NuxtPhotoOptions } from '@lupinum/nuxt-photo' ``` `NuxtPhotoOptions` contains exactly `localImages`, `autoImports`, `components`, `css`, `lightbox`, `validation`, `provider`, and `labels`, all optional. Their types and defaults are in the [configuration reference](/raw/docs/reference/configuration-and-labels.md). --- Source: https://nuxt-photo.lupinum.com/docs/reference/css --- title: "CSS" description: "Every CSS class and CSS variable - lightbox mat, colors, buttons, glow, carousel, album - and the none, structure, and all CSS modes." url: "https://nuxt-photo.lupinum.com/docs/reference/css" route: "/docs/reference/css" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # CSS > Every CSS class and CSS variable - lightbox mat, colors, buttons, glow, carousel, album - and the none, structure, and all CSS modes. Nuxt Photo ships three CSS strategies, controlled by the `css` module option: ```ts [nuxt.config.ts] nuxtPhoto: { css: 'structure' // 'none' | 'structure' | 'all' } ``` | Value | Loaded content | | --- | --- | | `'none'` | No stylesheets. You write every rule yourself. | | `'structure'` | Layout + geometry only. **Default.** | | `'all'` | Structure + the default theme. | ## Class naming Ready-made components use `np-` classes with double-underscore and double-dash notation. PhotoGroup has a `display: contents` wrapper without a built-in class. Primitives also expose the data attributes listed below: ``` .np-album → the album root .np-album--rows → layout modifier (also --columns, --masonry) .np-album__row .np-album__column .np-album__item → each photo wrapper .np-album__img → the inner .np-album__skeleton → measured-layout mount placeholder .np-album__end → end-reached sentinel .np-trigger → native PhotoTrigger button .np-photo → root .np-photo__img .np-photo__caption .np-lightbox → the lightbox root .np-lightbox__backdrop → the room behind the photo .np-lightbox__ambient → soft glow of the current photo's colors .np-lightbox__ui → non-interactive full-screen chrome layer .np-lightbox__controls → wrapper around the buttons .np-lightbox__stage → wrapper around the viewport .np-lightbox__topbar .np-lightbox__counter .np-lightbox__actions .np-lightbox__tools → custom tools, configured tools, zoom, and close .np-lightbox__container .np-lightbox__viewport .np-lightbox__slide .np-lightbox__slide-spacer → preserves track positions for unmounted slides .np-lightbox__media → the full-screen gesture area (LightboxViewport) .np-lightbox__caption → the label under the photo .np-lightbox__label → caption content for the current photo .np-lightbox__btn .np-lightbox__btn--prev .np-lightbox__btn--next .np-lightbox__btn--zoom .np-lightbox__btn--close .np-lightbox__fallback → image load failure message [data-np-slide-zoom] → zoom and pan wrapper for each image [data-np-frame-area] → measured frame area; never painted [data-np-transition-layer] → internal FLIP transition visual .np-carousel → carousel root .np-carousel__viewport .np-carousel__container .np-carousel__slide .np-carousel__media → main slide .np-carousel__controls .np-carousel__arrow .np-carousel__arrow--prev .np-carousel__arrow--next .np-carousel__counter .np-carousel__autoplay → pause and play button for autoplay .np-carousel__caption .np-carousel__dots .np-carousel__thumb .np-carousel__thumbs .np-carousel__thumbs-viewport .np-carousel__thumbs-container .np-carousel__thumb-img .np-carousel__thumb--selected .np-carousel__dot .np-carousel__dot--selected ``` Interactive lightbox children opt back into pointer events inside the full-screen UI layer. Keep that pattern if you replace the included markup: a click beside the photo must reach the viewport, which closes the lightbox, while controls and captions stay clickable. You do not need to override these rules directly. Use CSS variables and the component `ui` object for class targets. ## CSS variables The theme CSS uses variables scoped to the relevant root. Override them globally in your app CSS or per-component. ### Lightbox The lightbox hangs each photo in a mat, like a framed print, with its caption underneath. The structure CSS sets the mat; the theme CSS sets the look. ```css .np-lightbox { /* Mat and caption room (structure CSS) */ --np-frame-inset-top: 72px; --np-frame-inset-bottom: calc(var(--np-caption-reserve) + 16px); --np-frame-inset-inline: clamp(80px, 7vw, 120px); --np-caption-reserve: 104px; /* height kept free for the caption */ --np-caption-gap: 14px; /* space between photo and caption */ /* Room (theme CSS) */ --np-backdrop-bg: #0c0c0e; --np-backdrop-filter: none; --np-ambient-opacity: 0.45; /* 0 turns the glow off */ /* Controls */ --np-btn-radius: 999px; --np-btn-bg: rgba(22, 22, 25, 0.62); --np-btn-hover-bg: rgba(48, 48, 54, 0.78); --np-btn-color: #f4f3f0; --np-btn-border: rgba(255, 255, 255, 0.12); --np-btn-shadow: 0 8px 24px rgba(0, 0, 0, 0.24); --np-btn-blur: 10px; --np-btn-disabled-opacity: 0.35; --np-focus-ring: #f4f3f0; --np-counter-color: rgba(244, 243, 240, 0.82); /* Photo */ --np-img-radius: 6px; --np-img-shadow: 0 30px 80px rgba(0, 0, 0, 0.45), 0 2px 10px rgba(0, 0, 0, 0.35); /* Caption */ --np-caption-color: #f4f3f0; --np-caption-heading-size: 17px; --np-caption-secondary: rgba(244, 243, 240, 0.66); } ``` Override any of them in your global CSS: ```css /* A tighter mat, a plain room, and square corners */ .np-lightbox { --np-frame-inset-inline: 72px; --np-ambient-opacity: 0; --np-img-radius: 0; } ``` Below 700 pixels of screen width the ready-made lightbox uses a smaller mat: `--np-frame-inset-top: 64px`, `--np-frame-inset-inline: 12px`, and `--np-caption-reserve: 96px`, with the bottom inset 12 pixels above the caption. The previous and next arrows are hidden there until they receive keyboard focus, because swiping changes the photo. While the lightbox is open, it also sets where the photo is drawn on its root, in viewport pixels: `--np-frame-x`, `--np-frame-y`, `--np-frame-width`, and `--np-frame-height`. The caption and arrows use them to stay next to the photo. `--np-caption-width` defaults to `min(max(var(--np-frame-width, 100vw), 320px), calc(100vw - 24px))`. It sets the caption width and keeps its inline position inside the viewport. ### Carousel ```css .np-carousel { --np-carousel-gap: 0.75rem; --np-carousel-slide-size: 100%; --np-carousel-slide-aspect: 16 / 10; --np-carousel-thumb-size: 5.5rem; --np-carousel-thumb-gap: 0.5rem; --np-carousel-radius: 0.5rem; --np-carousel-surface: rgba(0, 0, 0, 0.55); --np-carousel-surface-fg: #fff; --np-carousel-thumb-border: rgba(0, 0, 0, 0.1); } ``` Slide size, aspect, and thumb size can also be set via props (`slideSize`, `slideAspect`, `thumbSize`). The prop wins over the variable. ### Album Albums are mostly structural: spacing and padding come from props. For ready-made components, structure CSS is part of the layout contract, not decorative theme CSS. ## Why the structure CSS is required The structure CSS contains layout rules that are difficult to reproduce with Tailwind or other utility CSS: - Rows layout uses a trailing `` ghost to absorb remaining space. - Columns and masonry use nested `.np-album__column` children with explicit gap math. - The lightbox viewport coordinates CSS custom properties set by JavaScript (zoom state, pan offsets, gesture translate). The `css: 'none'` setting requires a complete replacement for these rules. Use it only with a custom gallery CSS system. ## Add classes with ui Ready-made components accept a `ui` object. Its classes merge with the built-in classes: ```vue ``` Use this option for hover states, border treatments, and shadow effects that do not need custom layout calculations. ## Style from scoped CSS Vue scoped styles rewrite class selectors. Therefore, a ` ``` ## Turn off all CSS ```ts [nuxt.config.ts] nuxtPhoto: { css: 'none' } ``` Without the structure CSS, the grid calculations fail and photos collapse to zero-size boxes. Select `'none'` only when you provide a complete replacement. `ui` keys are `root`, `img`, and `caption` for Photo; `root`, `item`, and `img` for PhotoAlbum; `root` for PhotoGroup; and `root`, `slide`, `img`, `thumb`, `caption`, and `controls` for PhotoCarousel. The object adds classes; it does not replace built-in geometry. Primitives use normal classes and slots; LightboxSlide also has effect, frame, and zoom wrapper class props. PhotoImage renders an available preview as an inline background image. The preview stays after a failed image request. Its background size is `cover`, with centered positioning and no repeat. --- Source: https://nuxt-photo.lupinum.com/docs/reference/package-exports --- title: "Package exports" description: "The supported import paths for Nuxt (@lupinum/nuxt-photo/app) and plain Vue (@lupinum/vue-photo), and everything each one exports." url: "https://nuxt-photo.lupinum.com/docs/reference/package-exports" route: "/docs/reference/package-exports" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Package exports > The supported import paths for Nuxt (@lupinum/nuxt-photo/app) and plain Vue (@lupinum/vue-photo), and everything each one exports. Use only these package entry points. Source folders and generated files are not public application imports. | Entry point | Purpose | | --- | --- | | `@lupinum/vue-photo` | Plain Vue components, helpers, and types. | | `@lupinum/vue-photo/styles.css` | Complete structure and theme CSS. | | `@lupinum/nuxt-photo` | Nuxt module. | | `@lupinum/nuxt-photo/app` | Application API, including usePhotoFolder. | | `@lupinum/vue-photo/agent-docs` | Docs for coding agents, see [AI coding agents](/raw/docs/start/ai-agents.md). | | `@lupinum/nuxt-photo/agent-docs` | Docs for coding agents, see [AI coding agents](/raw/docs/start/ai-agents.md). | ## Vue root runtime exports ```text [Runtime exports] Lightbox, LightboxAmbient, LightboxCaption, LightboxControls, LightboxOverlay, LightboxProvider, LightboxRoot, LightboxSlide, LightboxViewport, Photo, PhotoAlbum, PhotoCarousel, PhotoGroup, PhotoImage, PhotoTrigger, PhotoValidationError, createPhoto, definePhotoProvider, validatePhotos, responsive, useLightbox, usePhotoLabels ``` These are the complete runtime exports. The [types reference](/raw/docs/reference/types.md) lists the complete type-only exports. ```ts [app/main.ts] import { createPhoto, PhotoAlbum, responsive, type PhotoItem } from '@lupinum/vue-photo' import '@lupinum/vue-photo/styles.css' ``` ## Nuxt module root The only runtime export is `default`, the Nuxt module. The only type-only export is `NuxtPhotoOptions`. ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], }) ``` ## Nuxt application exports `@lupinum/nuxt-photo/app` re-exports every Vue root runtime and type-only export above, and adds exactly one runtime export: `usePhotoFolder`. Use this entry point for explicit Nuxt app imports. ```ts [app/utils/gallery.ts] import { responsive, type PhotoItem } from '@lupinum/nuxt-photo/app' export const columns = responsive({ 0: 2, 640: 3 }) ``` Nuxt registers Photo, PhotoAlbum, PhotoGroup, and PhotoCarousel by default. Its default auto-imports are useLightbox, usePhotoLabels, usePhotoFolder, and responsive. Lightbox must be imported explicitly. Lower-level components are auto-registered only with `components: { primitives: true }`. --- Source: https://nuxt-photo.lupinum.com/docs/help/troubleshooting --- title: "Troubleshooting" description: "Fix Nuxt Photo problems by error message or by what you see - invalid photo data, missing IDs in a PhotoGroup, missing lightbox context, Nuxt Image errors, hydration mismatches, unstyled albums, layout jumps, and lightbox clicks." url: "https://nuxt-photo.lupinum.com/docs/help/troubleshooting" route: "/docs/help/troubleshooting" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Troubleshooting > Fix Nuxt Photo problems by error message or by what you see - invalid photo data, missing IDs in a PhotoGroup, missing lightbox context, Nuxt Image errors, hydration mismatches, unstyled albums, layout jumps, and lightbox clicks. Search this page for the text of your error, or find what you see under [Symptoms](#symptoms). Nuxt Photo errors start with `[nuxt-photo]` or with the component name, such as `PhotoAlbum:`. ## Error messages ### `PhotoAlbum: photo at index 3 is missing a non-empty string id; use a stable ID from your data, not the array index` A photo has no `id`, or it is not a string. Use a stable ID from your data, such as the CMS asset ID, converted to a string. Do not use the array index: it changes when the order changes. ### `PhotoAlbum: photo "canyon" is missing a non-empty src` The photo has no image URL. Check the field you map to `src`. ### `PhotoAlbum: photo "canyon" has invalid width 0; use the real pixel width of the image file` or `PhotoAlbum: photo "canyon" has invalid height 0; use the real pixel height of the image file` `width` and `height` must be positive numbers: the real pixel size of the image file. A string such as `"1280"`, `0`, `null`, or `NaN` fails. Read the size from your CMS or at upload; see [Get the image size at upload](/raw/docs/guides/use-your-photos.md#get-the-image-size-at-upload). ### `PhotoAlbum: duplicate photo id "canyon" used at indexes 2, 7; IDs must be unique in one collection` Two photos in one collection share an `id`. Make IDs unique, or remove the duplicate record. ### `PhotoAlbum: photo "canyon" field "caption" must be a string` or `PhotoAlbum: photo "canyon" field "meta" must be an object` An optional field has the wrong type, often `null` from an API. Map `null` to `undefined`: `alt: asset.alt ?? undefined`. ### `[nuxt-photo] PhotoGroup descendant photo "canyon" is missing from the canonical photos collection. Add it to the photos of the surrounding , or render it outside the group. See https://nuxt-photo.lupinum.com/docs/reference/photo-group` A `Photo` or `PhotoAlbum` inside a `PhotoGroup` shows a photo that is not in the group's `photos`. Add it to the group's array, which also sets its place in the lightbox order: ```vue ``` ### `[nuxt-photo] `PhotoTrigger` requires an active lightbox context. Render it inside , or inside a lightbox component passed to a ready-made component. See https://nuxt-photo.lupinum.com/docs/reference/primitives` The same error names `LightboxRoot`, `LightboxViewport`, `Lightbox`, or another lower-level component. It renders outside a provider. Put it inside `LightboxProvider`, or use it in a lightbox component that you pass to a ready-made component's `lightbox` prop. ### `[nuxt-photo] No photo found at index 5` or `[nuxt-photo] No photo found for id "canyon"` `open()` or `openById()` got an index or ID that is not in the collection. The promise rejects with a `RangeError`; catch it when the value comes from a URL or user input. See [Link to a photo](/raw/docs/guides/control-the-lightbox.md#link-to-a-photo). ### `[nuxt-photo] PhotoAlbum: "lightbox" is a setup-time option; remount the component with a new key to change it` This development warning means you changed `lightbox` after the component mounted. Change the `key` together with it: ```vue ``` ### `[nuxt-photo] nuxtPhoto.provider requires @nuxt/image; install the module or omit the provider name.` Install `@nuxt/image` and add it to `modules`, or remove `nuxtPhoto.provider`. Without Nuxt Image, every image uses the photo's own `src`, `thumbSrc` and `srcset`. ### `[nuxt-photo] Unknown `nuxtPhoto.quality` option.` `nuxtPhoto` does not know this name, often because of a typo. Compare with [Configuration and labels](/raw/docs/reference/configuration-and-labels.md). Image quality, format and widths belong in your Nuxt Image `image` config; see [Deliver images](/raw/docs/guides/deliver-images.md). ### `[nuxt-photo] responsive() requires at least one breakpoint` `responsive({})` has no values. Give at least one width, such as `responsive({ 0: 2, 768: 3 })`. Keys must be finite numbers of 0 or more. ### `[nuxt-photo] PhotoCarousel autoplay.delayMs must be a positive finite number` `autoplay.delayMs` is `0`, negative, or not a number. The carousel also rejects a `loop` or `dragFree` that is not a boolean. ### Hydration mismatch in an album or image The server and the browser rendered different HTML. Usually an image URL contains the current time or a random value, or the photo list differs between server and browser. Create signed URLs before rendering and store them on the photo, load the photos with `useFetch` or `useAsyncData`, and keep `defaultContainerWidth` a fixed number. ## Symptoms ### The album is unstyled or moves after loading Keep the required structure CSS enabled: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@lupinum/nuxt-photo'], nuxtPhoto: { css: 'all' }, }) ``` Use `css: 'structure'` when your own CSS supplies the visual theme. Every photo must also have accurate `width` and `height` values. These values let the browser reserve space before the image loads. ### Selecting a photo does not open a lightbox `PhotoAlbum` and `PhotoGroup` open a lightbox by default. `Photo` and `PhotoCarousel` open one only when you set `lightbox`: ```vue ``` Lower-level components such as `PhotoTrigger` must be inside `LightboxProvider`. If `lightbox` was `false` when a ready-made component mounted, changing it to `true` does not create a provider. Remount the component with a new `:key`, or mount it only after the option is known. Lightbox capability is a setup-time option. ### Nuxt Image rejects a remote URL Add every remote image host to the Nuxt Image `domains` list. Use hostnames, not complete URLs: ```ts [nuxt.config.ts] export default defineNuxtConfig({ image: { domains: ['images.example.com'], }, }) ``` On Vercel, images whose width or height is above 8192 px are not optimized. ### A placeholder remains visible This is expected after the requested image fails. The placeholder stays behind the broken request instead of leaving an empty frame. It disappears after a successful load and shows again when the image request changes. Check the final image URL in the browser network panel. A successful request is required to remove the placeholder. ### A custom thumbnail ignores image configuration A raw `` bypasses Nuxt Photo's image provider, `srcset` and placeholder. Render `PhotoImage` in the slot, and pass it the `sizes` and `priority` that the `thumbnail` slot gives you. ### Thumbnails download files that are too large Open the network panel and compare a thumbnail's file width with its width on screen times the pixel density: - On Vercel, the smallest file is the smallest width in `image.screens`. Add small screens; see [Deploy on Vercel](/raw/docs/guides/deliver-images.md#deploy-on-vercel). - A `sizes` prop on `PhotoAlbum` replaces the layout's own value. Remove it. - A `` in a custom thumbnail slot with `sizes="100vw"` asks for screen-wide files. Use `PhotoImage` instead. ### Images appear late while scrolling The first request for each image width waits until the image service has transformed it. Check the response headers: on Vercel, `x-vercel-cache: MISS` means a transform, `HIT` a cached file. Open image-heavy pages once after you add many images. For remote images on Vercel, raise `minimumCacheTTL`; see [Deploy on Vercel](/raw/docs/guides/deliver-images.md#deploy-on-vercel). With IPX on your own server, keep `format` at `webp`, because AVIF encoding is slow. ### Local images return 404 on Vercel IPX runs inside a Vercel function, which cannot read `public/`. Use the `vercel` provider, which Nuxt Image selects by itself on Vercel, or generate a static site with `nuxi generate`. ### A Nuxt import fails under pnpm Nuxt applications should import public symbols from `@lupinum/nuxt-photo/app`. Importing `@lupinum/vue-photo` directly requires that package to be installed as a direct dependency. ### RTL changes do not update a carousel Components read the computed CSS direction of their root. `PhotoCarousel` reads it at mount. Set `dir` on an ancestor and remount the carousel when the direction changes: ```vue
``` Lightboxes read the direction again each time they open. ### A click closes the lightbox A click beside the photo closes the lightbox, and a click on the photo zooms to its real pixels. In a custom lightbox, a full-screen layer that takes pointer events catches those clicks first. Give such layers `pointer-events: none` and turn pointer events back on for the buttons inside them. ### The photos do not fade in a custom lightbox The fade modes stack the slides with rules from the structure CSS. Keep `css: 'structure'` or `css: 'all'`, or stack `[data-np-slide]` elements yourself. ### The opening animation jumps Keep the default `lightbox: { transition: 'auto' }`. It fades when the thumbnail is not visible enough for a reliable opening animation. Check that thumbnail IDs and dimensions match the active photo. Programmatic opening without a visible thumbnail also uses a fade. After a fix, test selecting a thumbnail, next and previous navigation, Escape, focus restoration, mobile width, and your application direction. --- Source: https://nuxt-photo.lupinum.com/docs/help/known-constraints --- title: "Known constraints" description: "What Nuxt Photo 1.0 does not do and which options are read only at mount - known image sizes, photos only, setup-time lightbox, one open lightbox, required structure CSS, limited carousel options, Vercel image limits, and sizes=auto support." url: "https://nuxt-photo.lupinum.com/docs/help/known-constraints" route: "/docs/help/known-constraints" locale: "en" section: "Documentation" collection: "docs" source: "ginko-content" --- # Known constraints > What Nuxt Photo 1.0 does not do and which options are read only at mount - known image sizes, photos only, setup-time lightbox, one open lightbox, required structure CSS, limited carousel options, Vercel image limits, and sizes=auto support. ## Known dimensions are required Every photo needs accurate intrinsic `width` and `height` values before it is rendered. Nuxt Photo does not measure images in the browser. Store them with the asset, or, in Nuxt, let `usePhotoFolder` or `localImages` read them from files in `public/` at build time. ## The photo model contains images only Video and mixed-media slides are outside the Nuxt Photo 1.0 model. Build those experiences separately instead of representing them as photos. ## Lightbox capability is decided at setup The `lightbox` option on ready-made components is read when the component is created. Remount the component to change that capability. Transition settings can change while the component remains mounted. ## PhotoGroup uses one explicit collection `PhotoGroup.photos` owns identity and navigation order. Descendant components register thumbnail and slide behavior, but do not add items to the collection. ## One lightbox owns the modal state Only one provider owns focus and page isolation at a time. Opening another lightbox closes the current owner. ## Structure CSS is required Use `css: 'all'` for the included appearance or `css: 'structure'` with your own theme. With `css: 'none'`, your application must recreate all structural album, carousel, lightbox, focus, and transition CSS. ## Carousel options are intentionally limited The public behavior options are `loop`, `dragFree`, and Nuxt Photo's autoplay settings. Embla plugins, private methods, and arbitrary Embla options are not extension points. ## Runtime direction changes must be explicit Every component reads computed CSS direction from its root. `PhotoCarousel` reads it at mount. Set `dir` on an ancestor or CSS `direction` on the gallery root. Remount the inline track when direction changes. Lightboxes read the gallery root direction on each open. ## Reduced motion can change animation behavior Nuxt Photo follows `prefers-reduced-motion` and reacts when the media query changes. Opening and closing use a short fade instead of moving from the thumbnail, unless `lightbox.transition` is `'none'`. With `navigation: 'slide'` the next photo appears without scrolling, and the fade modes change photos without a fade. Carousel autoplay does not run. ## The fade modes need the structure CSS `navigation: 'fade'` and `'crossfade'` stack the slides with rules from the structure CSS. A custom lightbox that replaces all structural CSS must stack `[data-np-slide]` elements itself. ## The backdrop glow needs readable thumbnails The included theme blurs a small copy of the current thumbnail behind the lightbox. A thumbnail from another origin is blurred only when its server sends CORS headers, such as `Access-Control-Allow-Origin: *`; otherwise the glow is only softened. Set `--np-ambient-opacity: 0` to turn the glow off. ## A failed image keeps its placeholder The placeholder stays visible after the main image fails. It shows again when the image request changes, and disappears after that request loads. ## Vercel does not optimize very large or unusual images Vercel's image optimization accepts sources up to 8192 px wide or high and returns at most 10 MB. It optimizes JPEG, PNG, WebP and AVIF; other formats are served as they are. Resize larger originals before you upload them. See [Vercel's limits](https://vercel.com/docs/image-optimization/limits-and-pricing). ## Not every browser measures lazy images for `sizes` Thumbnails use `sizes="auto, px"`. Browsers that support `auto` use the measured width of lazy images. The others use the pixel width from the layout, which is exact for `PhotoAlbum` and an estimate for ``. These constraints keep server rendering, navigation order, and modal ownership predictable. Use [Troubleshooting](/raw/docs/help/troubleshooting.md) for symptom-based fixes.