ComponentsThemes
Palette
Default
Stone
Rose
Blue
Green
Violet
Yellow
Orange
6

← Components

Alert

A box that displays a callout message with optional title and description.

Installation

bin/rails g wabi:add alert

Example

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.