Simple Svelte Responsive Image Gallery
Tags: svelte, webdev, javascript
Published: 2021-11-03 11:54:12

---
title: "Simple Svelte Responsive Image Gallery"
published: "true"
description: "Simple Svelte responsive image gallery: create a ribbon gallery, using Svelte dimension bindings to maintain the aspect ratio of all images."
tags: "svelte, webdev, javascript"
canonical_url: "https://rodneylab.com/simple-svelte-responsive-image-gallery/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kawq2eoamwiezt0yp7c5.png"
---

## ✨ Simple Svelte Responsive Image Gallery: Introduction

We look at a simple Svelte responsive image gallery in this post.  By simple I mean to say the functionality is simple.  Despite that it lets us explore a couple of Svelte and Vite features which are a little more advanced.  In particular we look at glob importing where we can import, for example all files matching a certain pattern into SvelteKit JavaScript code.  We also use Svelte dimension bindings to make sure all images from our gallery &mdash; tall and wide &mdash; look good, maintaining aspect ratio as the window size changes.  As well as being responsive the images will be generated in Next-Gen formats.  Finally we add an optimisation which should help with the Core Web Vitals Largest Contentful Paint metric.  I should also mention we add lazy loading as another Core Web Vitals optimisation.

Generally, I prefer to roll my own components whenever possible, rather than leaning on libraries so really enjoyed putting this tutorial together.  If you are looking for a simple scrolling gallery, supporting modern image formats that is responsive, this should do the trick.  Even if you are looking for a fully-featured light box, you will probably find parts here which you can recycle for use with your own code or library.

## ⚙️ Getting Started

There's a bit to get through so let's get going!  I have used a script to generate image data automatically to speed things up, so you will need to download those image data files as well as the images themselves in a moment.  First though let's spin up a new skeleton project:

```shell
pnpm init svelte@next sveltekit-simple-image-gallery && cd $_
pnpm install
pnpm install @fontsource/inter @rodneylab/sveltekit-components svelte-feather-icons vanilla-lazyload vite-imagetools
pnpm run dev
```

From the options, choose **Skeleton project**, **Use TypeScript:? No**, **Add ESLint...? Yes** and **Add Prettier...? Yes**.  As well as set up Svelte, we have installed a font and a Svelte component library to help with generating responsive image boiler plate.  Together with those two packages, we have some icons for the next / previous buttons to move between images.  Finally there's a couple of packages to help with lazy loading and Next-Gen image generation and caching.

As an extra bit of setup, update `svelte.config.js` for use with `vite-imagetools`:

```javascript
import { imagetools } from 'vite-imagetools';
 
/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    vite: {
      plugins: [imagetools({ force: true })],
    },
  },
};
 
export default config;
```

### File Download

Lastly create a `src/lib/assets/` folder and <a aria-label="Open the folder containing the images" href="https://github.com/rodneylab/sveltekit-simple-image-gallery/tree/main/src/lib/assets">download the six images from that location in the Git repo</a>.  Finally create `src/lib/generated` and <a aria-label="Open the folder containing the Java Script files" href="https://github.com/rodneylab/sveltekit-simple-image-gallery/tree/main/src/lib/generated">repeat, copying the JavaScript files from the equivalent folder on the GitHub repo</a>.  Typically you would want to generate these files in a script, updating for required images formats, widths and pixel densities, though I have done this already to save time.  You can <a aria-label="Open the mentioned script on Git Hub" href="https://github.com/rodneylab/sveltekit-simple-image-gallery/blob/main/generate-responsive-image-data.js">take a look at the script which generates this data</a> including low resolution placeholders in the repo.

## 🔨 Server Route

Next we will create a server route.  This file will look for the JavaScript image data files we just downloaded and generate a single array image data (spanning all images).  Create the file at `src/routes/index.json.js` and add the following content:

```javascript
export async function get() {
  try {
    const images = await import.meta.glob('../lib/generated/*.js');
    const imageDataPromises = Object.keys(images).map((path) => images[path]());
    const imageData = await Promise.all(imageDataPromises);
    const imageDataArray = imageData.map((element) => element.default);
 
    return {
      body: JSON.stringify({ data: imageDataArray }),
    };
  } catch (error) {
    console.error('Error: ', error);
    return {
      status: 500,
      error: `Error in index.json data retrieval: ${error}`,
    };
  }
}
```

There are one or two interesting things in here.  In line `3`, we are importing all of the JavaScript files in the `lib/generated` folder.  To do this, we use a <a aria-label="Read Vite docs on glob imports"  href="https://vitejs.dev/guide/features.html#glob-import">Vite Glob Import</a>.  Essentially, Vite expands this to an object:

```javascript
const images = {
  '../lib/generated/image-1.js': () => import('../lib/generated/image-1.js'),
  '../lib/generated/image-2.js': () => import('../lib/generated/image-2.js'),
  '../lib/generated/image-3.js': () => import('../lib/generated/image-3.js'),
  '../lib/generated/image-4.js': () => import('../lib/generated/image-4.js'),
  '../lib/generated/image-5.js': () => import('../lib/generated/image-5.js'),
  '../lib/generated/image-6.js': () => import('../lib/generated/image-6.js'),
}
```

Each of the members of the object is a key-value pair, with the key being the path for one of the files in our folder.  The value in each case is the import function, so to complete the import, we need to call the function on each field. We do that in line `4`, generating a promise for each file we import and mapping all the promises to an array.


Over the following lines, we extract the default export from each of the files <a aria-label="Open M D N docs on Promise.all" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all">using the Promises API</a>.  If this is your first time using `async/await`, <a aria-label="Read roe about async await" href="https://rodneylab.com/sveltekit-image-plugin/#creatingPlaceholderImages">you might find the explanation in the post on the SvelteKit Image plugin useful</a>.

Our endpoint generates an array of image data which we will use next on the home page.

## 🏠 Home Page Svelte

Next, we will replace the code in `src/routes/index.svelte` with the following:

```html
<script context="module">
  export const load = async ({ fetch }) => {
    try {
      const response = await fetch('/index.json', {
        method: 'GET',
        credentials: 'same-origin',
      });
      return {
        props: { ...(await response.json()) },
      };
    } catch (error) {
      console.error(error);
    }
  };
</script>
  
<script>
  import { browser } from '$app/env';
  import RibbonGallery from '$lib/components/RibbonGallery.svelte';
  import '@fontsource/inter';
  import { onMount } from 'svelte';
  import lazyload from 'vanilla-lazyload';
 
  export let data;
 
  onMount(() => {
    if (browser) {
      document.lazyloadInstance = new lazyload();
    }
  });
 
  // import image data for caching images
  (async () => {
    await import.meta.glob('../lib/generated/*.js');
  })();
</script>
 
<svelte:head>
  <title>Basic Responsive Ribbon Gallery</title>
  <html lang="en-GB" />
  <meta
    name="description"
    content="Demo site for basic responsive image library with a ribbon layout"
  />
</svelte:head>
 
<div class="container">
  <header class="header"><h1>Basic Responsive Ribbon Gallery</h1></header>
  <main><RibbonGallery {data} /></main>
</div>
 
<style>
  :global(html) {
    font-family: 'Inter';
    background: #006e90;
    min-height: 100vh;
  }
 
  :global(body) {
    margin: 0;
  }
 
  .container {
    max-width: 1280px;
    margin: 0 auto;
  }
 
  .header {
    background: #01200f;
    color: #eef5db;
    font-weight: 900;
    padding: 1rem 1.5rem;
  }
</style>
```

In line `1`&ndash;`15` we have a <a aria-label="Read docs on Svelte load functions" href="https://kit.svelte.dev/docs#loading">standard SvelteKit load function</a> in which we get the image data array from our endpoint.

The `onMount` function is called when our home page is created.  We initialise our lazyload at this point.  You can <a aria-label="Open post on lazy loading i frames in Svelte Kit" href="https://rodneylab.com/lazy-loading-iframes-sveltekit/">see more on this in the post on Lazy loading iframes in SvelteKit</a>.

Lines `32`&ndash;`35` probably seem pointless as we do not use the result anywhere.  In these lines, we are importing the files we use in the endpoint to generate the image data array.  In fact we only do this import here <a aria-label="Read more about how to cache assets in Svelte Kit" href="https://kit.svelte.dev/faq#assets">to ensure the images are cached</a>. You might find you can omit this code running in dev mode, but switch to build and have no images!

In line `49` we add our image gallery component to the DOM.  Let's add the code for this and a couple of ancillary components to our project next.

## 🧩 Simple Svelte Responsive Image Gallery Components

We will use <a aria-label="Open the Feather Icons site" href="https://feathericons.com/">feather icons</a> for our forward and previous user interface buttons.  Create a folder at `src/lib/components` then add `NextIcon.svelte` and `PreviousIcon.svelte` to the folder, and paste in this code:

```html
<script>
  import { SkipForwardIcon } from 'svelte-feather-icons';
</script>
 
<SkipForwardIcon size="32" />
```

```html
<script>
  import { SkipBackIcon } from 'svelte-feather-icons';
</script>
 
<SkipBackIcon size="32" />
```

We're almost done now!  Next step is to add the final missing piece; the gallery component.

## 🖼 Ribbon Gallery Component

The image gallery will have a few features to make the pictures look their best.  This includes preserving the image aspect ratio when the window is resized and keeping all images the same height as we scale.  As well as that we want to ensure that for a small-screened device, the widest image in the gallery can be displayed, without panning.  For this to happen, we need to work out which is the widest image and use its aspect ratio to set the height for all of the images.  To get all of this right, we will use Svelte dimension binding.  There is a little maths (math) involved, but it's not too complex.

Lets start putting the image component together.  Create a `src/lib/components/RibbonGallery.svelte` file and paste in the following code:

```html
<script lang>
  import { browser } from '$app/env';
  import NextIcon from '$lib/components/NextIcon.svelte';
  import PreviousIcon from '$lib/components/PreviousIcon.svelte';
  import { Image } from '@rodneylab/sveltekit-components';
  import { afterUpdate, onMount } from 'svelte';
 
  export let data;
 
  let containerHeight;
  let containerWidth;
 
  let maxAspectRatio = 1.0;
 
  $: aspectRatios = data.map((element) => {
    const { width, height } = element;
    const aspectRatio = width / height;
    if (aspectRatio > maxAspectRatio) {
      maxAspectRatio = aspectRatio;
    }
    return aspectRatio;
  });
 
  $: height = 512;
  $: calculateHeight;
  function calculateHeight() {
    if (containerHeight && containerWidth) {
      const maxHeight = containerHeight - 59;
      height =
        containerWidth / maxHeight < maxAspectRatio ? containerWidth / maxAspectRatio : maxHeight;
    }
  }
 
  onMount(() => {
    if (browser && document.lazyloadInstance) {
      document.lazyloadInstance.update();
      calculateHeight();
    }
  });
 
  afterUpdate(() => {
    calculateHeight();
  });
 
  $: widths = [...aspectRatios.map((element) => parseFloat((element * height).toFixed(2)))];
  $: sizes = [...widths.map((element) => `${element}px`)];
  $: currentIndex = 0;
  $: imageTitle = data[currentIndex].title;
```

Here in lines `10` & `11` we create variables which we need to hold the measurements for our container height and width.  Then at lines `15` to `22` we have a utility function to work out the image with highest aspect ratio.  Aspect ratio is width divided by height, so the widest image has the largest aspect ratio.

### Image Height

Next in line `24`&ndash;`32` we work out what height our images should have.  To start the ball rolling, we set an initial height of `512px`.  In a moment we will see that we bind `containerHeight` and `containerWidth` to the actual DOM object dimensions.  Because of that, we need to wait for the DOM to be ready, before we have a value (hence the guard in line `27`).  The element we measure will have the images on top and some controls to shuffle through the images below.  In between there might be some space, depending on the browser window height.  We always want to allow some space for the controls below so in determining in the height for our images, we subtract the height of the controls (`59px`) in line `28`.

![Simple Svelte Responsive Image Gallery: Screenshot: Image shows gallery in short, wide browser window.  Three portrait-orientation images and a single landscape-orientation image are shown side-by-side.  Below are previous and next controls.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qxe99fs2w671s31nobv4.png)

Moving on to the code in line `30`.  Let's call the difference between the height of our measured element and the height of the controls the maximum height. Generally, we want the images to be as big as possible, so try to set their height to be equal to the maximum height. In line&nbsp;`30`, we look at the widest image and if we find it is just too wide to display at maximum height (without having to pan), we reduced the height of all the images.  The height we choose is back calculated from the width of our element and the aspect ratio of this widest image.

So, this block is just working out when we need to reduce the image height, and what that reduced height should be.  We call the `calculateHeight` function when the component first mounts (line `37`) and then again when it updates (line `42`), to keep the height good.

### Previous, Next Image Logic

Let's add some logic to move between images next, by pasting this code at the bottom of the same file:

```html
  const imageCount = data.length;
 
  function advanceIndex() {
    currentIndex = (currentIndex + 1) % imageCount;
  }
 
  function regressIndex() {
    currentIndex = (currentIndex + imageCount - 1) % imageCount;
  }
   
  function prefersReducedMotion() {
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  }
 
  function scrollToNextImage() {
    advanceIndex();
    if (prefersReducedMotion()) {
      document
        .getElementById(`image-${currentIndex + 1}`)
        .scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' });
    } else {
      document
        .getElementById(`image-${currentIndex + 1}`)
        .scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' });
    }
  }
 
  function scrollToPreviousImage() {
    regressIndex();
    if (prefersReducedMotion()) {
      document
        .getElementById(`image-${currentIndex + 1}`)
        .scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' });
    } else {
      document
        .getElementById(`image-${currentIndex + 1}`)
        .scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'start' });
    }
  }
</script>
```

In lines `53` and `57` we are using the modulus operation (`%`) so we can loop around to the first or last image when we get to the last image.  I really love the way Svelte handles animation and makes it easy to add some polish to image transitions in image galleries.  Here though in-built HTML functionality is pretty good and we will rely on that.  In particular <a aria-label="Read M D N docs on sroll into view" href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView">we are using `element.scrollIntoView()`</a>.

For this API to work, we add a unique id to each of our images and scroll to the `id` of whichever image we choose.  The rest just works!  If you have a lot of images though and scroll from the first to last, scrolling can be quite quick when smooth scrolling in switched on!  If the user prefers reduced motion, we revert to `auto` which scrolls a little slower.  That's all the JavaScript so let's add some HTML markup now.

### Svelte Dimension Binding

Paste this svelte code at the bottom of the same file:

```html
<div class="container" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight}>
  <div class="ribbon">
    <section class="images">
      {#each data as { alt, src, sources, placeholder }, index}
        <div id={`image-${index + 1}`}>
          <Image
            {alt}
            width={widths[index]}
            {height}
            {src}
            {sources}
            {placeholder}
            sizes={sizes[index]}
            loading={index === 0 ? 'eager' : 'lazy'}
            importance={index === 0 ? 'high' : 'auto'}
            maxWidth="1280px"
          />
        </div>
      {/each}
    </section>
  </div>
  <section class="info">
    <div class="controls">
      <span class="prev-next-button">
        <button
          on:click={() => {
            scrollToPreviousImage();
          }}><PreviousIcon /><span class="screen-reader-text">previous image</span></button
        ></span
      >
      <p>{currentIndex + 1} of {imageCount}</p>
      <span class="prev-next-button">
        <button
          on:click={() => {
            scrollToNextImage();
          }}><NextIcon /><span class="screen-reader-text">next image</span></button
        ></span
      >
    </div>
    <div class="title-text"><h1>{imageTitle}</h1></div>
  </section>
</div>
```

We saw previously we had dimensions of the container element in the JavaScript for this component.  In line `91` you see how we bind the Svelte measured dimension to the JavaScript variable.  Once more Svelte makes something that could very complicated very simple.  Be careful not to use this where it is not necessary as it comes with a performance hit.  Learn more <a aria-label="Read Svelte Tutorial on dimension binding" href="https://svelte.dev/tutorial/dimensions">about Svelte dimension bindings in Svelte docs</a>.

### Image Load Optimisation

We have a few image loading optimisations here to <a aria-label="Read more about Core Web Vitals on Wed dot dev" href="https://web.dev/vitals/">help improve Core Web Vitals together with the user experience</a> as well as SEO of your app.  We already mentioned images are lazy loaded.  This means the user's browser initially only loads the images that are in view.  The others are only loaded when the user scrolls over.  The `vanilla-lazyload` plugin helps with this.  On top we give a hint to the browser in line `104` to load images lazily.  We want the user to see something when the page first load so the first image loads eagerly.

Next, we add low resolution placeholders.  Together with width and height data, which we supply, this lets the browser know how much space to reserve for the images, reducing cumulative layout shift.  Because we want the image to scale to the browser width and maintain aspect ratio, there is some potential for CLS for any elements below the images in the DOM.  Bear this is mind if you use this code for other projects.

Finally <a aria-label="Read about priority hints on Web dot dev"  href="https://web.dev/priority-hints/">we set `importance` to high</a> for the first image in line `105`.  This is another hint to the browser to give the user something to see quicker and should help to improve the First Contentful Paint metric.

As an aside, in line `95` we add a unique id to each image to help with the scroll into view function we looked at earlier.


### Style

The last part is to add style.  Unlike some other tutorials on this site, styling is needed here for the gallery to work as expected.  This is mostly because we set heights on some elements.  To finish off paste this CSS code at the end of the `RibonGallery.svelte` file:

```html
<style>
  .container {
    display: flex;
    flex-direction: column;
    height: calc(100vh - 7.12rem);
    max-width: 1280px;
    width: 100%;
    margin: 0 auto;
  }
 
  .ribbon {
    display: flex;
    width: 100%;
  }
 
  .images {
    display: flex;
    overflow: hidden;
  }
 
  .info {
    display: flex;
    align-items: center;
    margin: auto 1rem 1rem;
    color: #eef5db;
  }
 
  .info button {
    border-style: none;
    background: transparent;
  }
 
  .info p,
  .info h1 {
    margin-top: 0;
    margin-bottom: 0;
  }
  .info h1 {
    font-size: 1.563rem;
  }
 
  .controls {
    display: flex;
    align-items: center;
    padding-right: 0.5rem;
    width: max-content;
  }
 
  .controls .prev-next-button {
    display: flex;
    vertical-align: middle;
    color: #eef5db;
  }
 
  .controls button {
    color: #eef5db;
    padding: 0.75rem 0.5rem;
    cursor: pointer;
  }
 
  .title-text {
    padding-right: auto;
  }
 
  .screen-reader-text {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    clip-path: inset(50%);
    height: 1px;
    margin: -1px;
    width: 1px;
    overflow: hidden;
    position: absolute !important;
    word-wrap: normal !important;
  }
</style>
```

That's all the code and everything should work now.  Give it a try!

## 💯  Simple Svelte Responsive Image Gallery: Testing

That's it, mission complete (apart from testing).  First we want to make sure the controls work for moving between images.  Make sure you can bring all the images into view using the previous and next buttons.   Then try resizing the browser window.  All images should maintain aspect ratio as you make the window larger or smaller.

![Simple Svelte Responsive Image Gallery: Screenshot: Image shows gallery in tall, narrow browser window.  A single landscape-orientation image takes the full window width.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b1uyb10soeo1gyzolncl.png)

The final test is to make the browser window tall and narrow and scroll to the fourth image.  It should span the width of the window.  You should not need to pan to see the entire image.

If that's all work let's recap and look at some extensions.

## 🙌🏽 Simple Svelte Responsive Image Gallery: What we Learned

In this post we saw:

- how you can bind the dimensions of an element to a JavaScript variable in Svelte,

- a way to import all the files in a particular  using Vite glob imports,

- how to optimise images for Core Web Vitals and better user experience.

I do hope there is at least one thing in this article which you can use in your work or a side project.  As an extension you might consider infinitely looping the images, so you don't get the disjoint scroll when you reach the last image.  You would have to anticipate reaching the last image and tack the first image onto the end of the array (and something similar for scrolling backwards past the first image).

You can see the <a aria-label="Open the Rodney Lab Git Hub repo" href="https://github.com/rodneylab/sveltekit-simple-image-gallery">full code for this using Simple Svelte Responsive Image Gallery tutorial on the Rodney Lab Git Hub repo</a>.  As always get in touch with feedback if I have missed a trick somewhere!

## 🙏🏽 Simple Svelte Responsive Image Gallery: Feedback

Have you found the post useful? Do you have your own methods for solving this problem? Let me know your solution. Would you like to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="Contact me on Twitter" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a> as well as other topics. Also <a aria-label="Subscribe to the Rodney Lab newsletter" href="https://rodneylab.com/about/#newsletter">subscribe to the newsletter to keep up-to-date</a> with our latest projects.