DEV Community

Cover image for Creating a CSS-only Card Slider with Tailwind CSS
Cruip
Cruip

Posted on • Originally published at cruip.com

Creating a CSS-only Card Slider with Tailwind CSS

Live Demo / Download

A card slider is a clever component that displays multiple layers of information. What makes this element unique is the ability to showcase an extensive amount of content in a limited space, and make the information accessible in a single click.

This approach, like many others, doesn’t work well with extensive content, but not all, so it’s up to you to decide when to implement it and how.

In this tutorial, we’ll show you how to create a CSS-only common variant of this component using Tailwind CSS, and we will guide you on how to efficiently and intuitively use it for your projects.

Just a heads-up before we dive in. We’re drawing inspiration from this awesome Codepen created by Julia Geisendorf for our tutorial. Now, since we’re sticking to a pure CSS approach, I gotta be upfront: the widget we’re crafting might not hit the perfect accessibility mark.

Ready? Let’s get started!

Figuring out the component structure

Alrighty, before we dive into coding, it’s important to outline the structure of the component we’re about to build and decide on the native HTML elements we’ll be using.

Since we’re aiming to create a clickable stack of cards without relying on JavaScript, we’ll employ a set of radio buttons to manage the active card’s state. Additionally, we’ll attach a label to each card, serving as the trigger to activate the respective card. Once this groundwork is laid, we’ll apply transition effects to make the animations visually engaging.

Let’s be honest here – managing all these elements solely with CSS is going to be a bit tricky, and there might be necessary adjustments or enhancements, especially when integrating additional cards into the slider. As mentioned earlier, it’s crucial to view this component as more of an experimental project rather than a solution ready for production websites.

So, let’s proceed to establish the structure of our component:

  <section class="px-12">
      <div class="max-w-lg mx-auto relative">

          <input id="article-01" type="radio" name="slider" class="sr-only peer/01" checked>
          <input id="article-02" type="radio" name="slider" class="sr-only peer/02">
          <!-- ... more radio buttons  -->

          <div class="absolute inset-0 scale-[67.5%] z-20">
              <label class="absolute inset-0" for="article-01"><span class="sr-only">Focus on the big picture</span></label>
              <article>Card content</article>
          </div>

          <div class="absolute inset-0 scale-[67.5%] z-20">
              <label class="absolute inset-0" for="article-02"><span class="sr-only">Focus on the big picture</span></label>
              <article>Card content</article>
          </div>

          <!-- ... more cards -->

      </div>
  </section>
Enter fullscreen mode Exit fullscreen mode

Alright, let’s break it down. We have a set of <input type="radio"> elements. These are hidden from view using the Tailwind CSS class sr-only. These inputs play a crucial role as triggers to activate our corresponding cards.

Alongside these inputs, we have a series of div containers housing our cards. These containers are absolutely positioned and layered on top of each other. Additionally, we’ve attached a label to each card. These labels serve as the mechanism for activating the respective cards, and they cover the entire card area by utilizing the absolute inset-0 classes.

It’s worth noting that we’ve already set up classes like peer/01, peer/02, and so forth. These pseudo-classes are key players in styling the card associated with the active input. This is achieved by leveraging the peer-checked modifier on the corresponding div.

Defining the style for the active card

Let’s start by assuming that the initial card is the active one – no coincidence here, the first radio button holds the checked attribute. With this foundation, let’s explore how we can employ the peer-checked modifier to define the style for this active card:

  <div class="
      absolute inset-0 scale-[67.5%] z-20
      peer-checked/01:relative
      peer-checked/01:z-50
      peer-checked/01:translate-x-0
      peer-checked/01:scale-100
      peer-checked/01:[&>label]:pointer-events-none            
  ">
      <label class="absolute inset-0" for="article-01"><span class="sr-only">Focus on the big picture</span></label>
      <article>Card content</article>
  </div>
Enter fullscreen mode Exit fullscreen mode

In a nutshell, we’re giving instructions to the card that when its corresponding radio button gets checked, it needs to be relatively positioned, get a higher z-index compared to the rest, maintain zero translation, and maintain a scale of 1. Also, we’re specifying that the label for the active card should not be clickable; otherwise, users won’t be able to interact with the elements within the card.

These classes will be duplicated for each card, with the only change being the reference number for the peer-checked modifier. For instance, for the second card, we’ll have something like this:

  <div class="
      absolute inset-0 scale-[67.5%] z-20
      peer-checked/02:relative
      peer-checked/02:z-50
      peer-checked/02:translate-x-0
      peer-checked/02:scale-100
      peer-checked/02:[&>label]:pointer-events-none            
  ">
      <label class="absolute inset-0" for="article-02"><span class="sr-only">Focus on the big picture</span></label>
      <article>Card content</article>
  </div>
Enter fullscreen mode Exit fullscreen mode

Styling the sibling cards to the active one

Now comes the tricky part. We need to define the style for the cards adjacent to the active one. To achieve this, we’ll be adding a set of custom classes to each card. It might not be the most elegant solution, but it’s the only option we’ve got to bring this component to life with pure CSS.

Before we dive in, let’s summarize how the style of the sibling cards should shape up:

  • Card -3: Should maintain the translation value of Card -2 (-translate-x-40), and fade out (opacity-0).
  • Card -2: Should translate left (-translate-x-40), have a lower z-index compared to the active card (z-30). Scaling isn’t necessary here, as it’ll inherit the minimal scaling set by scale-[67.5%].
  • Card -1: Should translate left (-translate-x-20), scale down (scale-[83.75%]), and have a lower z-index compared to the active card (z-40).
  • Card attiva
  • Card -1: Should translate right (translate-x-20), scale down (scale-[83.75%]), and have a lower z-index compared to the active card (z-40).
  • Card +2: Should translate right (translate-x-40), have a lower z-index compared to the active card (z-30). Scaling isn’t necessary here, as it’ll inherit the minimal scaling set by scale-[67.5%].
  • Card +3: Should maintain the translation value of Card +2 (translate-x-40), and fade out (opacity-0).

Following this blueprint, we’ll define custom classes for each card. So, let’s complete the first card:

  <div class="
      absolute inset-0 scale-[67.5%] z-20
      peer-checked/01:relative
      peer-checked/01:z-50
      peer-checked/01:translate-x-0
      peer-checked/01:scale-100
      peer-checked/01:[&>label]:pointer-events-none
      peer-checked/02:-translate-x-20
      peer-checked/02:scale-[83.75%]
      peer-checked/02:z-40
      peer-checked/03:-translate-x-40
      peer-checked/03:z-30
      peer-checked/04:-translate-x-40
      peer-checked/04:opacity-0
      peer-checked/05:-translate-x-40
  ">
      <label class="absolute inset-0" for="article-01"><span class="sr-only">Focus on the big picture</span></label>
      <article>Card content</article>
  </div>
Enter fullscreen mode Exit fullscreen mode

So, when the active card is the second one (peer-checked/02:), we need to apply the conditions previously outlined for the Card -1.

When the active card is the third one (peer-checked/03:), we’ll apply the conditions previously specified for the Card -2.

And so on.

Now, let’s wrap up styling for the second card:

  <div class="
      absolute inset-0 scale-[67.5%] z-20
      peer-checked/01:translate-x-20
      peer-checked/01:scale-[83.75%]
      peer-checked/01:z-40
      peer-checked/02:relative
      peer-checked/02:z-50
      peer-checked/02:translate-x-0
      peer-checked/02:scale-100
      peer-checked/02:[&>label]:pointer-events-none
      peer-checked/03:-translate-x-20
      peer-checked/03:scale-[83.75%]
      peer-checked/03:z-40
      peer-checked/04:-translate-x-40
      peer-checked/04:z-30
      peer-checked/05:-translate-x-40 
      peer-checked/05:opacity-0
  ">
      <label class="absolute inset-0" for="article-01"><span class="sr-only">Focus on the big picture</span></label>
      <article>Card content</article>
  </div>
Enter fullscreen mode Exit fullscreen mode

So, this logic needs to be extended to all the other cards too. It’s not overly complex, but it does demand a careful eye.

Managing focus with keyboard navigation

As we mentioned earlier, this component we’re building isn’t perfectly accessible. However, we can definitely enhance its accessibility by highlighting the active card when the corresponding radio button is focused.

To achieve this, all we need to do is add the classes peer-focus-visible/01:[&_article]:ring and peer-focus-visible/01:[&_article]:ring-indigo-300 to each card. These classes will apply a focus ring to the active card and remove it when the card is no longer active. As you may guess, we’ll need to replicate these classes for each card, changing the reference number of the peer-focus-visible modifier.

Therefore, when the active card receives focus, you’ll be able to navigate between the cards using the keyboard’s right and left arrow keys, or the up and down arrow keys.

Ceveats – Since we never hide the inactive cards using the display: none property, this creates an issue during tab navigation. For example, if you’ve focused on card 3 and press the tab key, the focus will move to the first interactive element of card 1, instead of navigating within the content of card 3. This is an undesired behavior that, unfortunately, cannot be resolved with CSS alone.

Putting the complete code together

Now that we’ve explained how the component works and how it’s structured, let’s put together the complete code:

  <section class="px-12">
      <div class="max-w-lg mx-auto relative">

          <input id="article-01" type="radio" name="slider" class="sr-only peer/01">
          <input id="article-02" type="radio" name="slider" class="sr-only peer/02">
          <input id="article-03" type="radio" name="slider" class="sr-only peer/03" checked>
          <input id="article-04" type="radio" name="slider" class="sr-only peer/04">
          <input id="article-05" type="radio" name="slider" class="sr-only peer/05">

          <div class="
              absolute inset-0 scale-[67.5%] z-20 transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]
              peer-focus-visible/01:[&_article]:ring
              peer-focus-visible/01:[&_article]:ring-indigo-300
              peer-checked/01:relative
              peer-checked/01:z-50
              peer-checked/01:translate-x-0
              peer-checked/01:scale-100
              peer-checked/01:[&>label]:pointer-events-none
              peer-checked/02:-translate-x-20
              peer-checked/02:scale-[83.75%]
              peer-checked/02:z-40
              peer-checked/03:-translate-x-40
              peer-checked/03:z-30
              peer-checked/04:-translate-x-40                    
              peer-checked/04:opacity-0
              peer-checked/05:-translate-x-40
          ">
              <label class="absolute inset-0" for="article-01"><span class="sr-only">Focus on the big picture</span></label>
              <article class="bg-white p-6 rounded-lg shadow-2xl">
                  <header class="mb-2">
                      <img class="inline-flex rounded-full shadow mb-3" src="./icon.svg" width="44" height="44" alt="Icon" />
                      <h1 class="text-xl font-bold text-slate-900">Focus on the big picture</h1>
                  </header>
                  <div class="text-sm leading-relaxed text-slate-500 space-y-4 mb-2">
                      <p>
                          Many desktop publishing packages and web page editors now use Pinky as their default model text, and a search for more variants will uncover many web sites still in their infancy.
                      </p>
                      <p>
                          All the generators tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.
                      </p>
                  </div>
                  <footer class="text-right">
                      <a class="text-sm font-medium text-indigo-500 hover:underline" href="#0">Read more -></a>
                  </footer>
              </article>
          </div>

          <div class="
              absolute inset-0 scale-[67.5%] z-20 transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]
              peer-focus-visible/02:[&_article]:ring
              peer-focus-visible/02:[&_article]:ring-indigo-300                        
              peer-checked/01:translate-x-20
              peer-checked/01:scale-[83.75%]
              peer-checked/01:z-40
              peer-checked/02:relative
              peer-checked/02:z-50
              peer-checked/02:translate-x-0
              peer-checked/02:scale-100
              peer-checked/02:[&>label]:pointer-events-none
              peer-checked/03:-translate-x-20
              peer-checked/03:scale-[83.75%]
              peer-checked/03:z-40
              peer-checked/04:-translate-x-40
              peer-checked/04:z-30
              peer-checked/05:-translate-x-40 
              peer-checked/05:opacity-0
          ">
              <label class="absolute inset-0" for="article-02"><span class="sr-only">Focus on the big picture</span></label>
              <article class="bg-white p-6 rounded-lg shadow-2xl">
                  <header class="mb-2">
                      <img class="inline-flex rounded-full shadow mb-3" src="./icon.svg" width="44" height="44" alt="Icon" />
                      <h1 class="text-xl font-bold text-slate-900">Focus on the big picture</h1>
                  </header>
                  <div class="text-sm leading-relaxed text-slate-500 space-y-4 mb-2">
                      <p>
                          Many desktop publishing packages and web page editors now use Pinky as their default model text, and a search for more variants will uncover many web sites still in their infancy.
                      </p>
                      <p>
                          All the generators tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.
                      </p>
                  </div>
                  <footer class="text-right">
                      <a class="text-sm font-medium text-indigo-500 hover:underline" href="#0">Read more -></a>
                  </footer>
              </article>
          </div>

          <div class="
              absolute inset-0 scale-[67.5%] z-20 transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]
              peer-focus-visible/03:[&_article]:ring
              peer-focus-visible/03:[&_article]:ring-indigo-300                          
              peer-checked/01:translate-x-40
              peer-checked/01:z-30
              peer-checked/02:translate-x-20
              peer-checked/02:scale-[83.75%]
              peer-checked/02:z-40
              peer-checked/03:relative
              peer-checked/03:z-50
              peer-checked/03:translate-x-0
              peer-checked/03:scale-100
              peer-checked/03:[&>label]:pointer-events-none
              peer-checked/04:-translate-x-20
              peer-checked/04:scale-[83.75%]
              peer-checked/04:z-40
              peer-checked/05:-translate-x-40
              peer-checked/05:z-30                  
          ">
              <label class="absolute inset-0" for="article-03"><span class="sr-only">Focus on the big picture</span></label>
              <article class="bg-white p-6 rounded-lg shadow-2xl">
                  <header class="mb-2">
                      <img class="inline-flex rounded-full shadow mb-3" src="./icon.svg" width="44" height="44" alt="Icon" />
                      <h1 class="text-xl font-bold text-slate-900">Focus on the big picture</h1>
                  </header>
                  <div class="text-sm leading-relaxed text-slate-500 space-y-4 mb-2">
                      <p>
                          Many desktop publishing packages and web page editors now use Pinky as their default model text, and a search for more variants will uncover many web sites still in their infancy.
                      </p>
                      <p>
                          All the generators tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.
                      </p>
                  </div>
                  <footer class="text-right">
                      <a class="text-sm font-medium text-indigo-500 hover:underline" href="#0">Read more -></a>
                  </footer>
              </article>
          </div>

          <div class="
              absolute inset-0 scale-[67.5%] z-20 transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]
              peer-focus-visible/04:[&_article]:ring
              peer-focus-visible/04:[&_article]:ring-indigo-300                          

              peer-checked/01:translate-x-40 
              peer-checked/01:opacity-0

              peer-checked/02:translate-x-40
              peer-checked/02:z-30

              peer-checked/03:translate-x-20
              peer-checked/03:scale-[83.75%]
              peer-checked/03:z-40

              peer-checked/04:relative
              peer-checked/04:z-50
              peer-checked/04:translate-x-0
              peer-checked/04:scale-100
              peer-checked/04:[&>label]:pointer-events-none

              peer-checked/05:-translate-x-20
              peer-checked/05:scale-[83.75%]
              peer-checked/05:z-40
          ">
              <label class="absolute inset-0" for="article-04"><span class="sr-only">Focus on the big picture</span></label>
              <article class="bg-white p-6 rounded-lg shadow-2xl">
                  <header class="mb-2">
                      <img class="inline-flex rounded-full shadow mb-3" src="./icon.svg" width="44" height="44" alt="Icon" />
                      <h1 class="text-xl font-bold text-slate-900">Focus on the big picture</h1>
                  </header>
                  <div class="text-sm leading-relaxed text-slate-500 space-y-4 mb-2">
                      <p>
                          Many desktop publishing packages and web page editors now use Pinky as their default model text, and a search for more variants will uncover many web sites still in their infancy.
                      </p>
                      <p>
                          All the generators tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.
                      </p>
                  </div>
                  <footer class="text-right">
                      <a class="text-sm font-medium text-indigo-500 hover:underline" href="#0">Read more -></a>
                  </footer>
              </article>
          </div>  

          <div class="
              absolute inset-0 scale-[67.5%] z-20 transition-all duration-500 ease-[cubic-bezier(0.25,1,0.5,1)]
              peer-focus-visible/05:[&_article]:ring
              peer-focus-visible/05:[&_article]:ring-indigo-300                          
              peer-checked/01:translate-x-40 
              peer-checked/02:translate-x-40 
              peer-checked/02:opacity-0
              peer-checked/03:translate-x-40
              peer-checked/03:z-30
              peer-checked/04:translate-x-20
              peer-checked/04:scale-[83.75%]
              peer-checked/04:z-40
              peer-checked/05:relative
              peer-checked/05:z-50
              peer-checked/05:translate-x-0
              peer-checked/05:scale-100
              peer-checked/05:[&>label]:pointer-events-none
          ">
              <label class="absolute inset-0" for="article-05"><span class="sr-only">Focus on the big picture</span></label>
              <article class="bg-white p-6 rounded-lg shadow-2xl">
                  <header class="mb-2">
                      <img class="inline-flex rounded-full shadow mb-3" src="./icon.svg" width="44" height="44" alt="Icon" />
                      <h1 class="text-xl font-bold text-slate-900">Focus on the big picture</h1>
                  </header>
                  <div class="text-sm leading-relaxed text-slate-500 space-y-4 mb-2">
                      <p>
                          Many desktop publishing packages and web page editors now use Pinky as their default model text, and a search for more variants will uncover many web sites still in their infancy.
                      </p>
                      <p>
                          All the generators tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.
                      </p>
                  </div>
                  <footer class="text-right">
                      <a class="text-sm font-medium text-indigo-500 hover:underline" href="#0">Read more -></a>
                  </footer>
              </article>
          </div>                  
      </div>
  </section>
Enter fullscreen mode Exit fullscreen mode

Conclusions

Now that you have learned how to build a card slider like the one in the tutorial, you can use your creativity to create multiple variants of this component for your user interfaces.

Are you hungry for more guides, tips, and tricks like this one? Check out our section of Tailwind CSS tutorials

Top comments (0)