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.
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:
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
vite.config.js for use with
Lastly create a
src/lib/assets/ folder and download the six images from that location in the Git repo
. Finally create
. 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 take a look at the script which generates this data
including low resolution placeholders in the repo.
just downloaded and generate a single array image data (spanning all images). Create the file at
src/routes/+page.js and add the following content:
There are one or two interesting things in here. In line
3, we are
To do this, we use a Vite Glob Import
. Essentially, Vite expands this to an object:
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
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 using the Promises API
. If this is your first time using
async/await, you might find the explanation in the post on the SvelteKit Image plugin useful.
Our endpoint generates an array of image data which we will use next on the home page.
Next, we will replace the code in
src/routes/+page.svelte with the
onMount function is called when our home page is created. We initialise
our lazyload at this point. You can see more on this in the post on Lazy loading iframes in SvelteKit.
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 to ensure
the images are cached. You might find you can omit this code running in dev mode, but switch to preview
(after building) and have no images!
34 we add our image gallery component to the DOM. Let’s
add some global CSS then look at the gallery component code and a couple of ancillary components to
our project next.
Craete a new
src/lib.styles folder. Inside add
global.css with the following content:
We will use feather icons
via iconify for our forward and previous user interface buttons. Create a folder at
src/lib/components then add
PreviousIcon.svelte to the folder, and paste in this code:
src/lib/components/NextIcon.svelte — click to expand code.
src/lib/components/PreviousIcon.svelte — click to expand code.
We’re almost done now! Next step is to add the final missing piece; the 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:
Here in lines
11 we create variables which we need to hold the measurements
for our container height and width. Then at lines
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.
Next in lines
512px. In a moment we will see that we bind
containerWidthto 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
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
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
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
37) and then again when it updates (line
42), to keep the height
Let’s add some logic to move between images next, by pasting this code at the bottom of the same file:
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 we are using
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.
Paste this svelte code at the bottom of the same file:
91 you see how we bind the Svelte measured dimension to the
not to use this where it is not necessary as it comes with a performance hit. Learn more about Svelte dimension bindings in Svelte docs
We have a few image loading optimisations here to help improve Core Web Vitals together with the user experience
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 we set
importance to high
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
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.
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
That’s all the code and everything should work now. Give it a try!
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.
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 working let’s recap and look at some extensions.
In this post we saw:
- 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 full code for this using Simple Svelte Responsive Image Gallery tutorial on the Rodney Lab Git Hub repo . As always get in touch with feedback if I have missed a trick somewhere!
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 consider supporting me through Buy me a Coffee.
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 @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.