First things first, what is a Svelte CSS Image Slider? The slider part is just an image gallery
with a strip of thumbnails. Click a thumbnail, and you see a large preview of the image you
selected. The hover part is making use of a some future CSSC S S feature proposals already at an advanced stage to create an effect similar to a desktop computer toolbar. There, when you hover the mouse
pointer over an app icon, it shifts up a touch and simultaneously grows.
We use some code by Jhey Thompkins here to produce that same effect when we hover over an image preview. Jhey added sprinkles;
using the future CSS :has selector also to make the thumbnails
either side of the focussed one shift and grow, albeit not as much. Using just CSS to do this
that :has is not supported in Firefox at the time of writing
(although Chrome and Safari do support it). Despite this, we can still grow and shift the
focussed image in Firefox.
🏓 Svelte CSS Image Slider: What is a Bouncing Slider? #
The overscroll bouncing slider, is a little bonus for mobile device users. The hover effect
doesn’t look great on smaller screens. For that reason, we replace it with a slider. So
mobile visitors don’t miss out on the future CSS magic, we add the bounce effect to the
scroller. This is similar to an effect you might see on an iPhone, if you try to scroll past the
start or end. The scroll bounces back to the first or last image. We use some code Adam Argyle demoed at the CSS Day conference to get this effect, again only with CSS for a natural feel
🧱 Svelte CSS Image Slider: What are we Building? #
We will build the CSS image slider we just described with a few extras. A follower asked me to
write a tutorial on creating a node SvelteKit app which we can run on Ubuntu or another Linux
distribution. Taking that into account, I thought this would be a great opportunity to be able to
use the Sharp image processing package on the server. Sharp requires a NodeJS environment to run,
so is not well-suited to serverless deploys. For speed across all devices, we want to serve
responsive image sets. It can be painful to produce the inputs manually as static assets for all
Sharp comes to the rescue here. We run it on a Svelte server endpoint which resizes images to the
required size on the fly. We will see, later, that we can add a public cache directive, so
Cloudflare will be able to cache the generated images for us. This helps serve images efficiently,
without creating static assets in all the various formats and sizes.
You can code along or check open the site in StackBlitz to have a play (GitHub repo link further
postcss.config.cjs — click to expand code.
Then, update the Svelte Config file (svelte.config.js):
Besides the new font, there is nothing extraordinary in the layout file. The site will have a page
for each image with a URL like https://example.com/machu-picchu.
For that reason, we create a src/routes/[slug] dynamic route
folder. Create the +layout.svelte file in this new folder with the
src/routes/[slug]/+layout.svelte — click to expand code.
Let’s take a look server side next. We mentioned briefly earlier that we will generate
responsive images on the fly. We will see, later, we can speed up page load but serving images
appropriate to the user device. Although this is a fantastic performance optimization, it can mean
generating quite a few resized images of different sizes for each actual image we want to include.
Consider the fact that we want to support devices with 340px wide
screens, as well as show images up to 1024px wide on desktops with
larger displays. On top, to support Retina and other high density displays, we also make images
available in double width too as well as in WebP and AVIFA V 1 Image File Format next-gen formats. A lot of image to include statically 🥵! That’s why we generate them.
To speed things up, we will include a rel=preload in the page
header and include a public caching header. This should encourage Cloudflare to use 103 Early hints as well as cache the images. Although things will be slow for the first visitor as images get
generated, subsequent visitors will the cached versions. As a further optimization, we will set
the cache header to immutable. This instruction tells browsers and
caching servers that we never expect to update the images. How, then, do we update them?
That’s where the hash part of the file path comes in! If we
change the file, we can expect the hash to change too, and the immutable directive on the old path will not matter.
src/routes/assets/[hash]/[filename]/server.ts — click to expand code.
In brief, we check for resize and format query strings in the request URL, so we can respond with the right next-gen format and size.
The Cache-Control header is most important here. This is where we
request Cloudflare cache for a very long time and not to check for updated images. Finally, Sharp
does the transformation magic, and we use streams to optimize serving the images.
Here, we use Vite JSON import to pull in image meta from a JSON file within the project. We return hrefs for all the
images (thumbnails and large preview) here. We include the hash mentioned earlier in these values.
Finally, we generate a low quality Base64 placeholder for each image. We will set these as the
background image, so the browser shows them while waiting for the full resolution images to load.
This is another speed optimization.
src/routes/[slug]/+page.server.ts — click to expand code.
Also, worth a mention is the use of Vite glob imports in line 11. This finds all *.jpg
files in the assets folder. Using a glob import instead of naming the files makes it easier to
swap out the images (we still need to update the JSON meta file though).
We will need a couple of utility functions to generate the Base64 low quality placeholders and
also image hashes. Although the MD5 hashing algorithm is no longer considered suitable for
cryptographic purposes, it is fine to use it here; an adversary has nothing to gain from finding
hash collisions for our images. Instead of using the full hash, we truncate it to just 10 characters, which should be sufficient for use in identifying changes in the images.
We use a Remix convention here in naming the server image utilities. While with Remix the .server portion means the code can only run on the server, as far as know, SvelteKit has no such
guarantee and the name is just to help us import the right files.
We mention the image source set briefly before. Including this lets the browser choose the most
adequate image for the device. We decide on image densities of 1
and 2, needed for Retina displays. Then, we have image widths of 280px, 334px, 672px, 768px, 1024px. These correspond to image sizes required for a small 360px wide screen, the iPhone XR (widest common mobile) and the desktop breakpoints. We have utility
functions to generate the sets, and the final code will look something like this:
Remember, the query strings in the image URLs tell our server endpoint what size and format to
serve. We opt for graceful degradation in the source sets, so start (optimistically) with AVIF,
the newest and smallest next-gen format. If the browser does not support this, then we fall back
to WebP and finally JPEG. For each set, the sizes attribute helps the browser decide which image
to download while it is still working out the page layout. Although we could possibly cut down on
the number of images here, we make the point that auto generation is a massive convenience.
Anyway, here is the component code (add it to src/lib/components/Image.svelte):
src/lib/components/Image.svelte — click to expand code.
This is where the exciting CSS is. We dissect it in the following sections. The markup is not too
different to the main image. You might notice we include stub bookend <div aria-hidden="true" class="overscroller" />. These are used in Adam Argyle’s bouncy scroll, We’ll come to them later!
Create src/lib/components/Thumbnails.svelte with the following
This snippet uses Svelte actions — a more convenient Sveltey way of adding a scroll into view query selector on
the active image. Create a src/lib/actions folder. In this new
directory, create scrollCurrentIntoView.ts with this code:
src/lib/components/Thumbnails.svelte — click to expand code.
Here’s the CSS code in full, in case you are following along. We will just pick out the most
interesting bits to talk about in the rest of this section. You can paste the code at the end of src/lib/components/Thumbnails.svelte
src/lib/components/Thumbnails.svelte — click to expand code.
We won’t go into detail on the :hover steps effect here.
Basically, it is set using lerp CSS custom properties. These are
defined in the CSS in src/routes/[slug]/+layout. Lerp stands for
linear interpolation and is a function which helps smooth motion in video games. Here Jhey uses it to for a natural , physical scaling on the focussed thumbnail and adjacent ones.
The main impact is on the flex-grow property , which fixes the relative sizes of the .thumbnails elements.
Tweak the flexflex syntax: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] property (the flex property is a recommended shorthand for flex-grow)
in line 107 as well as the --lerp custom properties in src/routes/[slug]/+layout.css to get a feel for how it impacts the elements.
overscroll-behavior-x: contain — this stops the whole
window moving when you hit the end of the scroller,
scroll-snap-type: x mandatory — use this to snap scrolling
at certain points (another nice alternative is proximity which
is less strict about things),
scroll-padding — adds padding to the scroll element,
scroll-snap-align — set on a thumbnail, controls whether
the thumbnail is centre aligned or edge aligned when scroll-snap-type on scroller is mandatory or proximity. We use centre here, but set it to start and end for first and last elements,
scroll-snap-stop — fantastic for an image slider, stops
you scrolling through dozens of thumbnails at once — make the viewer savour those
The .overscroller class is an Adam Argyle trick. Remember we added
bookend stub elements, to the slider? Those extend the slider slightly and the user exposes them
when they overscroll. However, we do not set scroll-snap-align on
them, so when they do get exposed, the browser automatically snaps to the next snappable
element, which is the second or second-last element (or the first or last real thumbnail). The
spring is provided natively by the browser. Is that not neat? To allow for the stub elements, we
set scroll-snap-align to start and
end for the second and penultimate element rather than first and
In this post, we saw how you can use sharp and future CSS to create a performant and
feature-packed Svelte CSS image slider. In particular, we saw:
some CSS code for :hover thumbnail effects,
how to add bouncy overscroll sliders,
quite a few optimizations for serving images.
Please clone the site from the Rodney Lab GitHub repo or open up the site on StackBlitz to try it out if you haven’t coded along. Hope you have found this post on creating a
Svelte CSS image Slider useful! In a follow-up post, we will see how to deploy our node SvelteKit
app to cloud hosting in a Linux box. We shall maintain the focus on performance as well as
consider security there. I hope you will join me, and am also keen to hear what else you are doing
with Svelte and ideas for future projects. Also let me know about any possible improvements to the
Yes, using PostCSS, you can start adding future CSS to your SvelteKit apps. We saw, once you install the PostCSS plugins, you need to add a `postcss.config.cjs` file to the project root folder. Then set `postcss: true` in your svelte.config.js config.preprocess options object. Finally, set the `lang=postcss` on your style tags in Svelte template files. That’s all you need!
How can you create a mobile bouncy overscroll effect in CSS? #
We have seen Adam Argyle’s trick is a fantastic way to create a performant, widely supported CSS overscroll effect. The effect we are talking about lets the user scroll past the final elements, and pings back once they let go. This is common on iOS devices, but we can make it available across the board using CSS. First, we add extra stub elements either end. We can size them and using the `inline-width` property and style them to be transparent, to fit the container. Then, we set `scroll-snap-type` to mandatory on the scroller element. Finally, we set `scroll-snap-align`, to `start`, for example on all elements except the two stub ones. Now, when the user overscrolls, the scroller bounces to the first or last element.
If you have found this post useful, see links below for further related content on this site. I do
hope you learned one new thing from the video. Let me know if there are any ways I can improve on
it. I hope you will use the code or starter in your own projects. Be sure to share your work on
Twitter, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for
other short videos you would like to see. Read on to find ways to get in touch, further below. If
you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.