Carousel
Slideshow with snap scrolling, prev/next controls, indicators, and optional autoplay.
Installation
bin/rails g wabi:add carouselExample
Slide 1
Slide 2
Slide 3
render Components::UI::Carousel.new(slide_count: 3, loop: true) do
render Components::UI::CarouselItemGroup.new do
3.times do |i|
render Components::UI::CarouselItem.new(index: i, class: "grid h-40 place-items-center rounded-md bg-muted text-2xl font-semibold") { "Slide #{i + 1}" }
end
end
render Components::UI::CarouselControl.new do
render Components::UI::CarouselPrevTrigger.new
render Components::UI::CarouselIndicatorGroup.new do
3.times { |i| render Components::UI::CarouselIndicator.new(index: i) }
end
render Components::UI::CarouselNextTrigger.new
end
end
Autoplay
Slide 1
Slide 2
Slide 3
render Components::UI::Carousel.new(slide_count: 3, loop: true, autoplay: true) do
render Components::UI::CarouselItemGroup.new do
3.times do |i|
render Components::UI::CarouselItem.new(index: i, class: "grid h-40 place-items-center rounded-md bg-muted text-2xl font-semibold") { "Slide #{i + 1}" }
end
end
render Components::UI::CarouselControl.new do
render Components::UI::CarouselPrevTrigger.new
render Components::UI::CarouselIndicatorGroup.new do
3.times { |i| render Components::UI::CarouselIndicator.new(index: i) }
end
render Components::UI::CarouselNextTrigger.new
end
end
Source
app/components/ui/carousel.rb
# 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
app/components/ui/carousel_item_group.rb
# 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
app/components/ui/carousel_item.rb
# 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
app/components/ui/carousel_control.rb
# 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
app/components/ui/carousel_prev_trigger.rb
# 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
app/components/ui/carousel_next_trigger.rb
# 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
app/components/ui/carousel_indicator_group.rb
# 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
app/components/ui/carousel_indicator.rb
# 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.