Skeleton
Loading placeholder — a pulsing muted box you size with utility classes.
Installation
bin/rails g wabi:add skeletonExample
div(class: "flex flex-col gap-3") do
render Components::UI::Skeleton.new(class: "h-12 w-12 rounded-full")
render Components::UI::Skeleton.new(class: "h-4 w-[250px]")
render Components::UI::Skeleton.new(class: "h-4 w-[200px]")
end
Source
app/components/ui/skeleton.rb
# frozen_string_literal: true
module Components
module UI
class Skeleton < Wabi::Base
variants do
base "animate-pulse motion-reduce:animate-none rounded-md bg-muted"
end
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
# role="status" is a live region that legitimately supports an accessible name,
# so aria-label is valid here (a bare <div> is role=generic and prohibits it) and
# AT announces the loading state. aria-busy signals loading; aria-label describes it.
# Callers can override any of these via **attrs (e.g. aria_label: "Loading avatar").
div(
role: "status",
"aria-busy": "true",
"aria-label": "Loading…",
**@attrs,
class: merge_class(tokens, user_class),
&
)
end
end
end
end
Accessibility
- It's a decorative placeholder: the root sets aria-busy="true" and a default aria-label="Loading…" (override via aria_label:) so assistive tech knows the region is loading rather than empty.
- Mark the surrounding region that swaps in the real content with role="status" or aria-live="polite" so screen readers announce when loading completes.
- The animate-pulse animation is paired with motion-reduce:animate-none, so it stops for users who prefer reduced motion.