Svelte Tiny Slider

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.

GitHub

Installation

Install using Yarn or NPM.

yarn add svelte-tiny-slider --dev npm install svelte-tiny-slider --save-dev

Include the slider in your app.

import { TinySlider } from "svelte-tiny-slider" <TinySlider> ... </TinySlider>

Usage

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>

Controls

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.

Controls via slot props

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:

  • setIndex is a function that accepts an index of the slide you want to navigate to.
  • currentIndex is an integer of the index you are current only on.

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>

Controls via exported props

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.

Styling

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.

Size

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>

Gap

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>

Content

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>

Lazy Loading

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>

Infinite Loading

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.

Event

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>

Properties

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 property. Once again this can be set using either let:distanceToEnd or bind:distanceToEnd.

<script>
  let distanceToEnd
  $: if (distanceToEnd && distanceToEnd < 500) console.log('Load more')
</script>

<TinySlider bind:distanceToEnd>
  ...
</TinySlider>

Other

Fill

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>

Transition Duration

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>

Threshold

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>

Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!
Card with links

I am some description to some topic that spans multiple lines.

Take me there!

Properties

This is a list of all configurable properties.

Property Default Description 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).

Functions

This is a list of exported functions.

Name Properties Description setIndex index Used to set the slider to the specified index.

Events

This is a list of events.

Name Description 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.