Svelte Tiny Slider is an easy to use highly customizable and unopinionated carousel or slider. There is little to no styling and how you structure your content is up to you. Images, videos, or any other element will work. Works with touch and keyboard controls. Made with accessibility in mind.
The package is less than 250 bytes gzipped (Bundlephobia) and has no dependencies.
This page contains code examples for both Svelte 4 and Svelte 5. Text descriptions are written with Svelte 4 in mind until Svelte 5 is officially released.
Install using Yarn or NPM.
yarn add svelte-tiny-slider@^1.0.0 --dev
npm install svelte-tiny-slider@^1.0.0 --save-dev
Include the slider in your app.
import { TinySlider } from "svelte-tiny-slider"
<TinySlider>
...
</TinySlider>
In it's most basic state the slider is just a horizontal carousel that can only be controlled through dragging the image either with your mouse or with touch controls. The carousel items can be whatever you want them to be, in this case we're using images.
<TinySlider>
{#each items as item}
<img src={item} alt="" />
{/each}
</TinySlider>
From this point there are several options to add any kind of controls you can think of. There two ways you can add controls. Either via slot props or via exported props using two way binds.
The easiest way is to use slot="controls"
and use it's slot props. There are several available props, but for controls the most relevant are:
In this example we are using svelte:fragment
but it could be any element you want it to be. Styling isn't included in this code example.
<TinySlider>
{#each items as item}
<img src={item} alt="" />
{/each}
<svelte:fragment slot="controls" let:setIndex let:currentIndex>
{#if currentIndex > 0}
<button on:click={() => setIndex(currentIndex - 1)}>...</button>
{/if}
{#if currentIndex < items.length - 1}
<button on:click={() => setIndex(currentIndex + 1)}>...</button>
{/if}
</svelte:fragment>
</TinySlider>
We could use the same props to implement some type of dots navigation.
<TinySlider>
{#each items as item}
<img src={item} alt="" />
{/each}
<div slot="controls" let:setIndex let:currentIndex>
{#each items as _, i}
<button
class:active={i == currentIndex}
on:click={() => setIndex(i)} />
{/each}
</div>
</TinySlider>
In a similar way we can also add thumbnail navigation.
<TinySlider>
{#each items as item}
<img src={item} alt="" />
{/each}
<div slot="controls" let:setIndex let:currentIndex>
{#each items as _, i}
<button
class:active={i == currentIndex}
on:click={() => setIndex(i)}
on:focus={() => setIndex(i)}>
<img src={item} alt="" height=60 />
</button>
{/each}
</div>
</TinySlider>
We can go one level deeper and use a slider for the controls of our slider. Here we are using the on:change event to move the thumbnails slider when the main slider also moves.
<TinySlider on:change={({ detail }) => thumbnailsSetIndex(detail)}>
{#each items as item}
<img src={item} alt="" />
{/each}
<div slot="controls" let:setIndex let:currentIndex let:reachedEnd>
<TinySlider gap="0.5rem" bind:setIndex={thumbnailsSetIndex}>
{#each items as item, i}
<button
class:active={i == currentIndex}
on:click={() => setIndex(i)}
on:focus={() => setIndex(i)}>
<img src={item} alt="" />
</button>
{/each}
</TinySlider>
{#if currentIndex > 0}
<button ...>...</button>
{/if}
{#if !reachedEnd}
<button ...>...</button>
{/if}
</div>
</TinySlider>
You don't have to control the component from a slot, you can control it from anywhere using two way binds. Declare any variable you want and bind them using bind
instead of let
. The variable currentIndex
can not be directly modified, it should only be used as a reference.
<script>
let setIndex
</script>
<TinySlider bind:setIndex>
{#each items as item}
<img src={item} alt="" />
{/each}
</TinySlider>
<button on:click={() => setIndex(2)}>...</button>
<button on:click={() => setIndex(5)}>...</button>
<button on:click={() => setIndex(9)}>...</button>
These buttons are not in a slot
and could be placed anywhere on your page.
There is very little css set by default, you're expected to bring your own. But to help you out there's a handful of props that might be of use. You don't need to use any of these, you could do it all with regular css, which we will also go over.
So far we've only been using one slide at a time. The number of sliders shown is not controlled by a prop, instead you can do it via css. To help you out there's the slot prop sliderWidth
. This is simply the document width of the slider element. Setting the width of your items to sliderWidth / 3
would cause 3 items to show at once. Once again this could be done with a slot prop or a two way bind, which ever you prefer.
<TinySlider let:sliderWidth>
{#each items as item}
<img src={item} width={sliderWidth / 3} />
{/each}
</TinySlider>
The gap prop allows you to set a gap between items. All this does is set the css property gap
, so alternatively you could do something like:
:global(.slider-content) {
gap: 10px;
}
But using the gap
prop might be more convenient. Accepts any css value.
<TinySlider gap="10px">
{#each items as item}
...
{/each}
</TinySlider>
We've been using images as examples so far, but the content can be anything. Any direct child of the slider will be considered a slide. Links and click events will not fire while dragging to prevent accidental clicks.
<TinySlider gap="0.5rem">
{#each { length: 20 } as _}
<div class="item">
<a href="https://svelte.dev" target="_blank">Link</a>
</div>
{/each}
</TinySlider>
When using images you might want to lazy load any images that are not visible. This can be done using native loading="lazy"
, but this comes with some drawbacks. To overcome these drawback there are several properties you can use.
For a simple slider you might use currentIndex
to hide any images that are above the current index.
<TinySlider let:currentIndex>
{#each items as item, i}
<div>
{#if currentIndex + 1 >= i}
<img src={item} alt="" />
{/if}
</div>
{/each}
...
</TinySlider>
Note how this is using currentIndex + 1 to preload one image ahead.
For sliders with multiple slides shown at once it might get more complicated when using currentIndex, especially when you might have different amounts of slides depending on the screen size. For that purpose you could use the shown property. This property returns an array of all indexes that have been onscreen at some point. Just like before this can be used either as let:shown or bind:shown.
<TinySlider let:shown>
{#each items as item, index}
<div>
{#if shown.includes(index)}
<img src={item} alt="" />
{/if}
</div>
{/each}
...
</TinySlider>
There are several properties you could use to implement infinite loading, meaning we load more items in when the user has scroll (almost) to the end of the slider.
You could use the event on:end, which fires when the user has reached the end of the slider based on pixels and not on currentIndex.
<TinySlider on:end={() => console.log('Reached end')}>
...
</TinySlider>
Similarity to the event you could use the property reachedEnd. This turns to true at the same time on:end is fired. Once again this can be set using either let:reachedEnd or bind:reachedEnd.
<script>
let reachedEnd = false
$: if (reachedEnd) console.log('Reached end')
</script>
<TinySlider bind:reachedEnd>
...
</TinySlider>
You might want to load more items before the user actually reaches the end to make it actually feel infinite. This could be achieved with the distanceToEnd or shown property. Once again this can be set using either let:distanceToEnd, bind:distanceToEnd.
<script>
let distanceToEnd
$: if (distanceToEnd && distanceToEnd < 500) console.log('Load more')
</script>
<TinySlider bind:distanceToEnd>
...
</TinySlider>
When showing multiple slides at once by default the slider will always fill out the full width when reaching the end. This behaviour can be disabled using fill={false}.
<TinySlider fill={false}>
...
</TinySlider>
The slider will always snap to the left side of one of the slides. The speed at which this happens can be set using the transitionDuration property. This value is given in milliseconds. This defaults to 300.
<TinySlider transitionDuration="1000">
...
</TinySlider>
When dragging the slider it will not transition to the next slide until a certain threshold has been passed to prevent accidental sliding. This also determines when a link or click event is disabled. This can be set using the threshold property. This value is given in pixels. This defaults to 30.
<TinySlider threshold="100">
...
</TinySlider>
This is a list of all configurable properties.
gap
0
Gap between each item. Can be any CSS value. fill
true
Boolean to set whether the slider is always filled fully when at the end. transitionDuration
300
Transition between items in milliseconds. threshold
30
Value in pixels for when you navigate when using drag controls. currentIndex
0
Index of the current slide (Read only). shown
[]
Array of all shown indexes (Read only). sliderWidth
0
Box width in pixels of the slider as it is on the page (Read only). maxWidth
0
Full width in pixels of all items together (Read only). currentScrollPosition
0
Current position in the slider in pixels (Read only). reachedEnd
false
Boolean that is set to true when you have reached the end of the slider (Read only). distanceToEnd
0
Distance in pixels until you reach the end of the slider (Read only).This is a list of exported functions.
setIndex
index
Used to set the slider to the specified index.This is a list of events.
end
Fired when the end of the slider has been reached. change
Fired when the slider changes it's index. The detail prop of the event is the current index.