Skip to main 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.

Find the right album layout

Resize the container and compare the seven layouts.

560px container
River canyon with moss-covered walls
Dandelion seed heads in evening light
Wall covered in open book pages
Orange tulips at the edge of a forest
Sea of clouds under a blue sky
Road through tall trees in fog
City skyline at dusk under a dramatic sky
Bicycle in front of a yellow shopfront
Green mountain ridge with a footpath

Choose a layout

LayoutWhat it doesUse it when
rowsPhotos share a row height and each row fills the width. Default.Photos have mixed shapes.
columnsPhotos 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.
masonryEqual-width columns. Each photo goes into the shortest column.Density matters more than strict order.
gridSame-size tiles in equal columns. Photos are cropped.Many photos must fit on one screen.
bentoBig and small tiles on a grid without gaps. Photos are cropped.A portfolio or project page.
mosaicA few photos fill one frame; the last tile shows how many more there are.A card, a post, or a header.
accordionSlices share one strip; the slice under the pointer or focus opens.A hero area with 3 to 8 photos.
vue
<PhotoAlbum :photos="photos" :layout="{ type: 'rows', targetRowHeight: 240 }" />
<PhotoAlbum :photos="photos" :layout="{ type: 'columns', columns: 3 }" />
<PhotoAlbum :photos="photos" :layout="{ type: 'masonry', columns: 4 }" />

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.

app/pages/gallery.vue
<template>
  <PhotoAlbum
    :photos="photos"
    :layout="{ type: 'grid', columns: responsive({ 0: 2, 640: 4 }), aspectRatio: 1 }"
  />
  <PhotoAlbum
    :photos="photos"
    :layout="{
      type: 'bento',
      columns: responsive({ 0: 2, 768: 4 }),
      featured: (photo) => photo.meta?.cover === true,
    }"
  />
  <PhotoAlbum :photos="photos" :layout="{ type: 'mosaic', aspectRatio: 16 / 9, max: 5 }" />
  <PhotoAlbum :photos="photos" :layout="{ type: 'accordion', aspectRatio: 2 }" />
</template>
LayoutOptionDefaultMeaning
gridcolumns3Columns. Accepts responsive().
gridaspectRatio1Width divided by height of every tile.
bentocolumns4Columns. Accepts responsive().
bentoaspectRatio4 / 3Width divided by height of one grid cell.
bentofeaturedthe first photo(photo, index) => boolean: photos that get a big tile.
mosaicaspectRatio4 / 3Width divided by height of the whole frame.
mosaicmax5Photos shown in the frame.
accordionaspectRatio2Width 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.

app/pages/gallery.vue
<template>
  <PhotoAlbum
    :photos="photos"
    :layout="{
      type: 'columns',
      columns: responsive({ 0: 2, 640: 3, 1024: 4 }),
    }"
    :spacing="responsive({ 0: 4, 640: 8, 1024: 12 })"
  />
</template>

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
<PhotoAlbum :photos="photos" :spacing="(width) => Math.min(16, Math.max(4, width / 80))" />

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
<PhotoAlbum
  :photos="photos"
  :default-container-width="1024"
  :layout="{ type: 'columns', columns: responsive({ 0: 2, 640: 3, 1024: 4 }) }"
/>

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
<PhotoAlbum
  :photos="photos"
  :spacing="(width) => (width < 640 ? 4 : 12)"
  :default-container-width="1024"
  :breakpoints="[375, 640, 1024, 1280]"
/>

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.