How to build an Alpine.js testimonial carousel with right‑edge controls

Build an accessible, horizontally scrolling testimonial carousel with Tailwind CSS and Alpine.js, and place the navigation buttons at the right edge of the layout.

Published on November 18, 2025 by Michael Andreuzza

Overview

This tutorial shows how to create a horizontally scrolling testimonial carousel using Tailwind CSS and Alpine.js, with navigation buttons aligned to the right edge of the section. It supports keyboard arrows, wraps around when you reach the end, and uses CSS snap for smooth paging.

We’ll cover:

  • Small Alpine shell (next, prev, to) using a measured offset
  • Right‑aligned controls that sit at the edge on large screens
  • The horizontal track with grid-flow-col and snap-x
  • Slide markup with headings, avatars, and quotes
  • Accessibility and optional focus management with x-intersect

Prerequisites: Tailwind CSS v3+ (or v4) and Alpine.js v3. For the optional focus behavior, install Alpine’s Intersect plugin.

We keep all behavior in a tiny Alpine object. to(strategy) calculates the next scroll position based on the first slide’s width and the CSS column gap.

html
<div
  x-data="{
    skip: 1,
    next() {
      this.to((current, offset, slider) => {
        const target = current + offset * this.skip;
        const max = slider.scrollWidth - slider.clientWidth; // small tolerance
        return target > max - 1 ? 0 : target;
      });
    },
    prev() {
      this.to((current, offset, slider) => {
        const target = current - offset * this.skip;
        return target < 0 ? Math.max(0, slider.scrollWidth - slider.clientWidth) : target;
      });
    },
    to(strategy) {
      const slider = this.$refs.slider;
      const current = slider.scrollLeft;
      const first = slider.firstElementChild;
      const gap = parseFloat(getComputedStyle(slider).columnGap) || 0;
      const offset = first.getBoundingClientRect().width + gap;
      const target = strategy(current, offset, slider);
      slider.scrollTo({ left: target, behavior: 'smooth' });
    },
    focusableWhenVisible: {
      'x-intersect:enter'() { this.$el.removeAttribute('tabindex'); },
      'x-intersect:leave'() { this.$el.setAttribute('tabindex', '-1'); },
    },
  }"
>
  <!-- content here -->
</div>

2 — Heading and right‑edge controls

Use a grid on large screens (lg:grid-cols-5). Place the buttons in column 5 and push them to the right with lg:ml-auto. For a more aggressive “peeking off the right edge” effect, you can absolutely position them with a small negative right value.

html
<div class="items-end grid grid-cols-1 lg:grid-cols-5">
  <div class="lg:col-span-2">
    <h2 class="text-xl md:text-2xl lg:text-3xl font-semibold tracking-tight text-zinc-900 text-balance">
      Hear from our customers about their experiences
    </h2>
    <p class="text-base mt-4 font-medium text-zinc-500">
      Find out how our users are using Oxbow UI to create a customized landing page for their business.
    </p>
  </div>
  <div class="inline-flex items-center mt-4 space-x-2 lg:ml-auto lg:col-start-5">
    <button class="size-9 p-2 rounded-md text-sm text-zinc-600 bg-zinc-50 outline outline-zinc-50 hover:bg-zinc-200 focus:outline-2 focus:outline-offset-2 focus:outline-zinc-600" x-on:click="prev">
      <span class="sr-only">Previous</span>
      <!-- left icon -->
    </button>
    <button class="size-9 p-2 rounded-md text-sm text-zinc-600 bg-zinc-50 outline outline-zinc-50 hover:bg-zinc-200 focus:outline-2 focus:outline-offset-2 focus:outline-zinc-600" x-on:click="next">
      <span class="sr-only">Next</span>
      <!-- right icon -->
    </button>
  </div>
</div>

<!-- Optional: outside‑edge variant on desktop -->
<div class="relative hidden lg:block">
  <div class="absolute -right-4 top-0 flex gap-2">
    <!-- buttons here (same as above) -->
  </div>
</div>

3 — Horizontal track

Use a horizontally scrolling ul that lays out slides in a single row via grid-flow-col and uses snap-x snap-mandatory for paging. Attach x-ref="slider" to control scroll position from Alpine.

html
<ul
  class="relative w-screen overflow-x-scroll gap-3 grid grid-flow-col auto-cols-max scrollbar-hide snap-mandatory snap-x"
  role="listbox"
  aria-labelledby="carousel-content-label"
  tabindex="0"
  x-ref="slider"
>
  <!-- slides -->
</ul>

4 — Slides

Each li uses snap-start and a fixed width (w-72). Use your preferred card surface: gradient + outline or plain.

html
<li role="option" aria-selected="false" class="h-full p-8 shadow bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl snap-start w-72">
  <figure class="flex flex-col-reverse justify-between h-full">
    <figcaption class="relative flex items-center mt-12 gap-3">
      <img alt="" src="/avatars/1.jpg" class="object-cover rounded-full size-12" loading="lazy" />
      <div>
        <h3 class="text-base font-medium text-zinc-900">Jane Doe</h3>
        <p class="text-xs font-medium text-zinc-400">Founder</p>
      </div>
    </figcaption>
    <blockquote class="text-base font-semibold text-zinc-900">Short, energetic quote about your product.</blockquote>
  </figure>
</li>

5 — Accessibility and focus handling

  • Wrap the carousel in a role="region" with a label; map arrow keys to prev/next.
  • Use role="listbox"/role="option" if you want SR users to perceive a selectable list.
  • Optional: pair Alpine’s Intersect plugin with focusableWhenVisible to keep off‑screen slides out of the tab order.
html
<div role="region" aria-labelledby="carousel-label" tabindex="0" x-on:keydown.left="prev" x-on:keydown.right="next"></div>

Full snippet

Paste the complete example below into a page that loads Tailwind + Alpine. The buttons sit on the right edge on large screens.

html
<div
  x-data="{
    skip: 1,
    next() {
      this.to((current, offset, slider) => {
        const target = current + (offset * this.skip)
        const maxScroll = slider.scrollWidth - slider.clientWidth
        // small tolerance for fractional pixels
        if (target > maxScroll - 1) return 0
        return target
      })
    },
    // go to previous page; wrap to last when before the start
    prev() {
      this.to((current, offset, slider) => {
        const target = current - (offset * this.skip)
        if (target < 0) return Math.max(0, slider.scrollWidth - slider.clientWidth)
        return target
      })
    },
    // helper that computes offset including grid gap and calls the strategy
    to(strategy) {
      const slider = this.$refs.slider
      const current = slider.scrollLeft
      const first = slider.firstElementChild
      const gap = parseFloat(getComputedStyle(slider).columnGap) || 0
      const offset = first.getBoundingClientRect().width + gap
      const target = strategy(current, offset, slider)
      slider.scrollTo({ left: target, behavior: 'smooth' })
    },
    focusableWhenVisible: {
      'x-intersect:enter'() {
        this.$el.removeAttribute('tabindex')
      },
      'x-intersect:leave'() {
        this.$el.setAttribute('tabindex', '-1')
      },
    },
  }"
>
  <div class="relative px-8 py-24 mx-auto md:px-12 lg:px-20 max-w-7xl">
    <div class="items-end grid grid-cols-1 lg:grid-cols-5">
      <div class="lg:col-span-2">
        <h2
          class="text-xl md:text-2xl lg:text-3xl font-semibold tracking-tight text-zinc-900 text-balance"
        >
          Hear from our customers about their experiences
        </h2>
        <p class="text-base mt-4 font-medium text-zinc-500">
          Find out how our users are using Oxbow UI to create a customized landing
          page for their business.
        </p>
      </div>
      <div
        class="inline-flex items-center mt-4 space-x-2 lg:ml-auto lg:col-start-5"
      >
        <button
          class="flex items-center justify-center text-center shadow-subtle font-medium duration-500 ease-in-out transition-colors focus:outline-2 focus:outline-offset-2 text-zinc-600 bg-zinc-50 outline outline-zinc-50 hover:bg-zinc-200 focus:outline-zinc-600 size-9 p-2 text-sm rounded-md"
          x-on:click="prev"
          tabindex="0"
        >
          <span class="sr-only">Skip to previous slide page</span>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            class="icon icon-tabler-chevron-left size-5"
            slot="icon"
          >
            <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
            <path d="M15 6l-6 6l6 6"></path>
          </svg>
        </button>
        <button
          class="flex items-center justify-center text-center shadow-subtle font-medium duration-500 ease-in-out transition-colors focus:outline-2 focus:outline-offset-2 text-zinc-600 bg-zinc-50 outline outline-zinc-50 hover:bg-zinc-200 focus:outline-zinc-600 size-9 p-2 text-sm rounded-md"
          x-on:click="next"
          tabindex="0"
        >
          <span class="sr-only">Skip to next slide page</span>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
            class="icon icon-tabler-chevron-right size-5"
            slot="icon"
          >
            <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
            <path d="M9 6l6 6l-6 6"></path>
          </svg>
        </button>
      </div>
    </div>
    <div class="flex flex-col-reverse w-full">
      <div
        class="flex flex-col-reverse w-full"
        aria-labelledby="carousel-label"
        role="region"
        tabindex="0"
        x-on:keydown.left="prev"
        x-on:keydown.right="next"
      >
        <div class="flex flex-col-reverse items-start lg:flex-row">
          <div class="w-full py-2 snap-x">
            <ul
              class="relative w-screen overflow-x-scroll gap-3 grid grid-flow-col auto-cols-max scrollbar-hide snap-mandatory"
              role="listbox"
              aria-labelledby="carousel-content-label"
              tabindex="0"
              x-ref="slider"
            >
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center mt-12 gap-3">
                    <img
                      alt="#_"
                      src="/avatars/1.jpg"
                      class="object-cover rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Mike Andreuzza
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Creator</p>
                    </div>
                  </figcaption>
                  <div>
                    <blockquote class="text-base font-semibold text-zinc-900">
                      Implementing Semplice's blockchain technology has been a
                      game-changer for our supply chain management.
                    </blockquote>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/2.jpg"
                      class="object-cover rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Gege Piazza
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Developer</p>
                    </div>
                  </figcaption>
                  <div>
                    <blockquote class="text-base font-semibold text-zinc-900">
                      Implementing Semplice's blockchain technology has been a
                      game-changer for our supply chain management.
                    </blockquote>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-linear-180 outline outline-zinc-100 from-zinc-50 to-zinc-100 rounded-xl snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center mt-12 gap-3">
                    <img
                      alt="#_"
                      src="/avatars/3.jpg"
                      class="object-cover rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Jenson Button
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Founder</p>
                    </div>
                  </figcaption>
                  <div>
                    <blockquote class="text-base font-semibold text-zinc-900">
                      We were initially hesitant about integrating blockchain
                      technology into our existing systems.
                    </blockquote>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/4.jpg"
                      class="object-cover mx-auto rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Johanna Manoy
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Designer</p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      Being in the financial industry, we were always looking for
                      ways to enhance our transactions' security and efficiency.
                    </p>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/5.jpg"
                      class="object-cover rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Sandra Piazza
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Creator</p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      Implementing Semplice's blockchain technology has been a
                      game-changer for our supply chain management.
                    </p>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/6.jpg"
                      class="object-cover mx-auto rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Yokiro Mariro
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Nutcracker</p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      We were initially hesitant about integrating blockchain
                      technology into our existing systems.
                    </p>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/11.png"
                      class="object-cover mx-auto rounded-full size-12"
                    />
                    <div>
                      <h3 class="text-base font-medium text-zinc-900">
                        Valentina Miu
                      </h3>
                      <p class="text-xs font-medium text-zinc-400">Writter</p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      Being in the financial industry, we were always looking for
                      ways to enhance our transactions' security and efficiency.
                    </p>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center gap-3">
                    <img
                      alt="#_"
                      src="/avatars/8.avif"
                      class="object-cover mx-auto rounded-full size-12"
                    />
                    <div>
                      <h3
                        class="text-lg font-medium tracking-tight text-zinc-900"
                      >
                        Gege Piazza
                      </h3>
                      <p class="mt-2 text-sm text-zinc-500">
                        Creator of Pizza Piazza
                      </p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      Implementing Semplice's blockchain technology has been a
                      game-changer for our supply chain management.
                    </p>
                  </div>
                </figure>
              </li>
              <li
                role="option"
                aria-selected="false"
                class="h-full p-8 shadow bg-zinc-50 snap-start w-72"
              >
                <figure class="flex flex-col-reverse justify-between h-full">
                  <figcaption class="relative flex items-center mt-12 gap-3">
                    <img
                      alt="#_"
                      src="/avatars/11.jpg"
                      class="object-cover mx-auto rounded-full size-12"
                    />
                    <div>
                      <h3
                        class="text-lg font-medium tracking-tight text-zinc-900"
                      >
                        Jenson Button
                      </h3>
                      <p class="mt-2 text-sm text-zinc-500">
                        Founder of Benji and Tom
                      </p>
                    </div>
                  </figcaption>
                  <div>
                    <p class="text-base">
                      We were initially hesitant about integrating blockchain
                      technology into our existing systems.
                    </p>
                  </div>
                </figure>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Wrap‑up

With just a few Alpine helpers and Tailwind’s utility classes, you get a responsive, accessible carousel. The right‑edge controls live independently of the track, and the to() helper makes your paging stable by reading the card width + gap at runtime.

/Michael Andreuzza

Did you like this post? Please share it with your friends!