Alert
A box that displays a callout message with optional title and description.
Installation
bin/rails g wabi:add alertExample
Heads up!
You can add components to your app using the wabi:add generator.
render Components::UI::Alert.new do
render Components::UI::AlertTitle.new { "Heads up!" }
render Components::UI::AlertDescription.new { "You can add components to your app using the wabi:add generator." }
end
Error
Your session has expired. Please log in again.
render Components::UI::Alert.new(appearance: :destructive) do
render Components::UI::AlertTitle.new { "Error" }
render Components::UI::AlertDescription.new { "Your session has expired. Please log in again." }
end
Source
app/components/ui/alert.rb
# frozen_string_literal: true
module Components
module UI
class Alert < Wabi::Base
# Consumer note: if you pass a decorative SVG icon as a direct child,
# add aria-hidden="true" and focusable="false" to that SVG element so
# screen readers skip the unlabelled graphic (e.g. <svg aria-hidden="true" focusable="false" ...>).
variants do
base "relative w-full rounded-lg border p-4 " \
"[&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] " \
"[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground"
variant :appearance, {
default: "bg-background text-foreground",
# Body text stays at `foreground` (always AA on `background` in every theme
# + dark mode); the destructive cue is the red border + red icon. Using
# `text-destructive` for the body failed WCAG 1.4.3 in dark mode (the
# button-tuned destructive red is too dark as text on a dark surface, ~2:1).
destructive: "bg-background text-foreground border-destructive/50 [&>svg]:text-destructive"
}, default: :default
end
def initialize(appearance: nil, **attrs)
@appearance = appearance
@attrs = attrs
end
# Live-region note: role="alert" (an implicit aria-live="assertive" region) only
# fires a screen-reader announcement when the element is *injected into the DOM
# after page load*. A statically server-rendered Alert that is present in the
# initial HTML is silently ignored by NVDA, JAWS, and VoiceOver.
# To announce dynamic alerts, inject the element via Turbo Stream or a Stimulus
# controller that appends it at runtime (do NOT rely on toggling visibility).
def view_template(&)
user_class = @attrs.delete(:class)
div(
role: "alert",
"aria-atomic": "true",
**@attrs,
class: merge_class(tokens(appearance: @appearance), user_class),
&
)
end
end
end
end
app/components/ui/alert_title.rb
# frozen_string_literal: true
module Components
module UI
class AlertTitle < Wabi::Base
variants { base "mb-1 font-medium leading-none tracking-tight" }
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
h5(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
app/components/ui/alert_description.rb
# frozen_string_literal: true
module Components
module UI
class AlertDescription < Wabi::Base
variants { base "text-sm [&_p]:leading-relaxed" }
def initialize(**attrs) = @attrs = attrs
def view_template(&)
user_class = @attrs.delete(:class)
div(**@attrs, class: merge_class(tokens, user_class), &)
end
end
end
end
Accessibility
- role="alert" — the panel is announced by assistive tech immediately on render.
- live-region semantics: dynamic insertions trigger immediate announcement.
- the "destructive" appearance is purely visual — it does NOT change the role or semantics.
- for non-urgent status, prefer role="status" or visually distinct UI without role="alert" to avoid screen-reader interruption.