ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Carousel

Slideshow with snap scrolling, prev/next controls, indicators, and optional autoplay.

Installation

bin/rails g wabi:add carousel

Example

Autoplay

Source

# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class Carousel < Wabi::Base
      def initialize(slide_count:, slides_per_page: 1, slides_per_move: 1, loop: false, orientation: :horizontal, autoplay: false, **attrs)
        @slide_count     = slide_count
        @slides_per_page = slides_per_page
        @slides_per_move = slides_per_move
        @loop            = loop
        @orientation     = orientation
        @autoplay        = autoplay
        @attrs           = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          **@attrs,
          data: {
            controller: "wabi--carousel",
            "wabi--carousel-slide-count-value":     @slide_count.to_s,
            "wabi--carousel-slides-per-page-value": @slides_per_page.to_s,
            "wabi--carousel-slides-per-move-value": @slides_per_move.to_s,
            "wabi--carousel-loop-value":            @loop.to_s,
            "wabi--carousel-orientation-value":     @orientation.to_s,
            "wabi--carousel-autoplay-value":        @autoplay.to_s,
          },
          class: merge_class("relative w-full", user_class)
        ) do
          yield if block_given?
          # Live-region announcer: screen readers hear "Slide X of N" on each transition.
          span(
            role: "status",
            "aria-live": "polite",
            "aria-atomic": "true",
            data: { "wabi--carousel-target": "announcer" },
            class: "sr-only"
          )
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselItemGroup < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--carousel-target": "itemGroup" },
          class: merge_class("flex", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselItem < Wabi::Base
      def initialize(index:, **attrs)
        @index = index
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--carousel-target": "item", "wabi-index": @index.to_s },
          class: merge_class("min-w-0 shrink-0 grow-0 basis-full", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselControl < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--carousel-target": "control" },
          class: merge_class("mt-3 flex items-center justify-between gap-4", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselPrevTrigger < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          "aria-label": "Previous slide",
          data: { "wabi--carousel-target": "prevTrigger" },
          class: merge_class(
            "inline-flex h-8 w-8 items-center justify-center rounded-full border border-input " \
            "disabled:pointer-events-none disabled:opacity-40 " \
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
            user_class
          )
        ) do
          if block_given?
            yield
          else
            raw safe(%(<svg aria-hidden="true" focusable="false" 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="h-4 w-4"><path d="m15 18-6-6 6-6"/></svg>))
          end
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselNextTrigger < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          "aria-label": "Next slide",
          data: { "wabi--carousel-target": "nextTrigger" },
          class: merge_class(
            "inline-flex h-8 w-8 items-center justify-center rounded-full border border-input " \
            "disabled:pointer-events-none disabled:opacity-40 " \
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
            user_class
          )
        ) do
          if block_given?
            yield
          else
            raw safe(%(<svg aria-hidden="true" focusable="false" 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="h-4 w-4"><path d="m9 18 6-6-6-6"/></svg>))
          end
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselIndicatorGroup < Wabi::Base
      def initialize(**attrs)
        @attrs = attrs
      end

      def view_template(&block)
        user_class = @attrs.delete(:class)
        div(
          data: { "wabi--carousel-target": "indicatorGroup" },
          class: merge_class("flex items-center gap-2", user_class)
        ) do
          yield if block_given?
        end
      end
    end
  end
end
# frozen_string_literal: true

require "date" # Phlex 2.4 references Date/Time constants lazily when rendering data:{} hashes

module Components
  module UI
    class CarouselIndicator < Wabi::Base
      def initialize(index:, **attrs)
        @index = index
        @attrs = attrs
      end

      def view_template
        user_class = @attrs.delete(:class)
        button(
          type: "button",
          "aria-label": "Go to slide #{@index.to_i + 1}",
          data: { "wabi--carousel-target": "indicator", "wabi-index": @index.to_s },
          class: merge_class(
            "h-2 w-2 rounded-full bg-border transition-colors motion-reduce:transition-none data-[current]:bg-primary " \
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
            user_class
          )
        )
      end
    end
  end
end

Accessibility

  • Root is aria-roledescription="carousel"; each slide announces its position.
  • Prev/Next/indicator buttons are keyboard-operable; autoplay pauses on focus/hover.